commit ea5b02ad0ae117e29d048e9e5ba5fb4fbe3d694b Author: valerpan Date: Mon Aug 10 14:29:38 2026 +0300 Initial commit ui-master diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b939794 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea/ +/out/ +/target/ +*.iml \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6f184c5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + org.example + HelpdeskUITest + 1.0-SNAPSHOT + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + + + + org.seleniumhq.selenium + selenium-java + 3.141.59 + + + org.testng + testng + 6.9.6 + + + io.qameta.allure + allure-testng + 2.13.8 + + + + + \ No newline at end of file diff --git a/src/main/java/elements/MainMenu.java b/src/main/java/elements/MainMenu.java new file mode 100644 index 0000000..ebbd1ee --- /dev/null +++ b/src/main/java/elements/MainMenu.java @@ -0,0 +1,66 @@ +package elements; + +import io.qameta.allure.Step; +import models.Ticket; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +/** Главное меню (блок элементов) */ +public class MainMenu { + + // Способ объявления элементов страницы, через аннотацию @FindBy (с последующей инициализацией) + + @FindBy(xpath = "//span[contains(text(),'New Ticket')]") + private WebElement newTicketButton; + + @FindBy(id = "userDropdown") + private WebElement logInButton; + + @FindBy(xpath = "//input[@id='search_query']") + private WebElement inputSearch; + + @FindBy(xpath = "//nav//button[@type='submit']") + private WebElement goButton; + + public MainMenu(WebDriver driver) { + /* Необходимо инициализировать элементы класса, аннотированные @FindBy. + Лучше всего это делать в конструкторе. */ + PageFactory.initElements(driver, this); + } + + @Step("Нажать кнопку создания новго тикета") + public void clickOnNewTicketButton() { + newTicketButton.click(); + } + + @Step("Нажать кнопку логина") + public void clickOnLogInButton() { + logInButton.click(); + } + + @Step("Найти тикет с помощью поиска") + public void searchTicket(Ticket ticket) { + setInputSearch(ticket.getTitle()) + .clickOnGoButton(); + } + + /* Если после вызова void метода, может потребоваться вызов другого метода этого же класса, + то можно вернуть сам класс и вызвать следующий метод через точку. */ + @Step("Ввести в поле поиска значение {text}") + public MainMenu setInputSearch(String text) { + inputSearch.sendKeys(text); + return this; + } + + @Step("Нажать кнопку поиска") + public void clickOnGoButton() { + goButton.click(); + } + + @Step("Получить логин пользователя") + public String loginedUser() { + return logInButton.getText(); + } +} diff --git a/src/main/java/models/Dictionaries.java b/src/main/java/models/Dictionaries.java new file mode 100644 index 0000000..ebfa662 --- /dev/null +++ b/src/main/java/models/Dictionaries.java @@ -0,0 +1,29 @@ +package models; + +import java.util.HashMap; + +/** Словари */ +public class Dictionaries { + + private static final HashMap priorities = new HashMap<>(); + private static final HashMap queues = new HashMap<>(); + + static { + priorities.put(1, "1. Critical"); + priorities.put(2, "2. High"); + priorities.put(3, "3. Normal"); + priorities.put(4, "4. Low"); + priorities.put(5, "5. Very Low"); + + queues.put(1, "Django Helpdesk"); + queues.put(2, "Some Product"); + } + + public static String getPriority(int priority) { + return priorities.get(priority); + } + + public static String getQueue(int queue) { + return queues.get(queue); + } +} diff --git a/src/main/java/models/Ticket.java b/src/main/java/models/Ticket.java new file mode 100644 index 0000000..fd9b334 --- /dev/null +++ b/src/main/java/models/Ticket.java @@ -0,0 +1,49 @@ +package models; + +import java.io.File; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class Ticket { + + /* Класс Ticket пакета models реализуем по аналогии c домашним заданием по тестированию API. + Класс должен содержать набор полей, необходимый для заполнения формы создания тикета. + Тип данных для каждого поля должен соответствовать документации swagger (см. раздел Models в документации). */ + + // todo: остальные поля класса + private String title; + private String due_date; + private File file; + + // todo: остальные геттеры и сеттеры + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDue_date() { + return due_date; + } + + // обычный сеттер + public void setDue_date(String due_date) { + this.due_date = due_date; + } + + // перегруженный сеттер, который принимает дату и форматирует её в строку по шаблону + public void setDue_date(LocalDateTime due_date) { + this.due_date = due_date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")); + } + + public File getFile() { + return file; + } + + public void setFile(File file) { + this.file = file; + } +} diff --git a/src/main/java/pages/AbstractPage.java b/src/main/java/pages/AbstractPage.java new file mode 100644 index 0000000..d1aa1bb --- /dev/null +++ b/src/main/java/pages/AbstractPage.java @@ -0,0 +1,13 @@ +package pages; + +import org.openqa.selenium.WebDriver; + +/** Элементы общие для всех страниц */ +public abstract class AbstractPage { + + protected static WebDriver driver; + + public static void setDriver(WebDriver webDriver) { + driver = webDriver; + } +} diff --git a/src/main/java/pages/CreateTicketPage.java b/src/main/java/pages/CreateTicketPage.java new file mode 100644 index 0000000..28447a0 --- /dev/null +++ b/src/main/java/pages/CreateTicketPage.java @@ -0,0 +1,39 @@ +package pages; + +import io.qameta.allure.Step; +import models.Ticket; +import org.openqa.selenium.WebElement; + +/** Страница создания тикета */ +public class CreateTicketPage extends HelpdeskBasePage { + + // todo: добавить элементам локтаоры через @FindBy + + private WebElement selectQueue; + + private WebElement inputProblem; + + // todo: добавить остальные поля формы + + private WebElement submitTicketButton; + + // todo: проинициализировать элементы + + @Step("Создать тикет") + public CreateTicketPage createTicket(Ticket ticket) { + setInputProblem(ticket.getTitle()); + // todo: заполнить остальные поля формы + clickOnSubmitButton(); + return this; + } + + @Step("Ввести имя проблемы: {text}") + public void setInputProblem(String text) { + inputProblem.sendKeys(text); + } + + @Step("Нажать на кнопку создания тикета") + public void clickOnSubmitButton() { + submitTicketButton.click(); + } +} diff --git a/src/main/java/pages/HelpdeskBasePage.java b/src/main/java/pages/HelpdeskBasePage.java new file mode 100644 index 0000000..aad4658 --- /dev/null +++ b/src/main/java/pages/HelpdeskBasePage.java @@ -0,0 +1,12 @@ +package pages; + +import elements.MainMenu; + +/** Элементы общие для системы Helpdesk */ +public class HelpdeskBasePage extends AbstractPage { + + /** Доступ к элементам главного меню */ + public MainMenu mainMenu() { + return new MainMenu(driver); + } +} diff --git a/src/main/java/pages/LoginPage.java b/src/main/java/pages/LoginPage.java new file mode 100644 index 0000000..88b3297 --- /dev/null +++ b/src/main/java/pages/LoginPage.java @@ -0,0 +1,55 @@ +package pages; + +import io.qameta.allure.Step; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +/** Страница логина */ +public class LoginPage extends HelpdeskBasePage { + + // поиск элемента через xpath + @FindBy(xpath = "//*[id='username']") + private WebElement user; + + // поиск элемента по id + @FindBy(id = "password") + private WebElement password; + + // поиск элемента через css + @FindBy(css = "[type='submit']") + private WebElement loginButton; + + public LoginPage() { + PageFactory.initElements(driver, this); + } + + /** + * Авторизация пользователя + * + * @param user логин пользователя + * @param password пароль пользователя + */ + @Step("Авторизация пользователя") + public LoginPage login(String user, String password) { + setUser(user); + setPassword(password); + clickOnLoginButton(); + return this; + } + + @Step("Ввести логин {user}") + private void setUser(String user) { + this.user.sendKeys(user); + } + + @Step("Ввести пароль") + private void setPassword(String password) { + this.password.sendKeys(password); + } + + @Step("Нажать кнопку авторизации") + private void clickOnLoginButton() { + this.loginButton.click(); + } +} diff --git a/src/main/java/pages/MainPage.java b/src/main/java/pages/MainPage.java new file mode 100644 index 0000000..b51665a --- /dev/null +++ b/src/main/java/pages/MainPage.java @@ -0,0 +1,12 @@ +package pages; + +import org.openqa.selenium.support.PageFactory; + +/** Главная страница */ +public class MainPage extends HelpdeskBasePage { + + public MainPage() { + PageFactory.initElements(driver, this); + } + +} diff --git a/src/main/java/pages/TicketPage.java b/src/main/java/pages/TicketPage.java new file mode 100644 index 0000000..c3dc41a --- /dev/null +++ b/src/main/java/pages/TicketPage.java @@ -0,0 +1,31 @@ +package pages; + +import io.qameta.allure.Step; +import models.Ticket; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** Страница отдельного тикета (авторизированный пользователь) */ +public class TicketPage extends HelpdeskBasePage { + + /* Верстка страницы может измениться, поэтому для таблиц вместо индексов строк и столбцов лучше использовать + более универсальные локаторы, например поиск по тексту + parent, following-sibling и другие. + + Текст тоже может измениться, но в этом случае элемент не будет найден и тест упадет, + а ошибку можно будет легко локализовать и исправить. + В случае изменений ячеек таблицы, локатор будет продолжать работать, но будет указывать на другой элемент, + поведение теста при этом изменится непредсказуемым образом и ошибку будет сложно найти. */ + private WebElement dueDate = driver.findElement(By.xpath("//th[text()='Due Date']/following-sibling::td[1]")); + + // todo: проинициализировать элементы через driver.findElement + private WebElement title; + private WebElement queue; + private WebElement email; + private WebElement priority; + private WebElement description; + + @Step("Проверить значение полей на странице тикета") + public void checkTicket(Ticket ticket) { + // todo: добавить реализацию метода + } +} diff --git a/src/main/java/pages/TicketsPage.java b/src/main/java/pages/TicketsPage.java new file mode 100644 index 0000000..df78a75 --- /dev/null +++ b/src/main/java/pages/TicketsPage.java @@ -0,0 +1,38 @@ +package pages; + +import io.qameta.allure.Step; +import models.Ticket; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +import java.util.List; + +/** Страница с таблицей тикетов и фильтрами */ +public class TicketsPage extends HelpdeskBasePage { + + // пример коллекции веб-элементов + @FindBy(xpath = "//div[@class='tickettitle']/a") + private List ticketsHref; + + public TicketsPage() { + PageFactory.initElements(driver, this); + } + + /** + * Ищем строку с и тикетом и нажимаем на нее + * + * @param ticket + */ + @Step("Открыть тикет с id {ticket.id}") + public void openTicket(Ticket ticket) { + String id = String.valueOf(ticket.getId()); + ticketsHref.stream() + .filter(WebElement::isDisplayed) + .filter(ticketHref -> ticketHref.getText().startsWith(id)) + .findFirst() + .orElseThrow(() -> new AssertionError("Не найден тикет с id " + id)) + .click(); + } + +} diff --git a/src/main/java/pages/ViewPage.java b/src/main/java/pages/ViewPage.java new file mode 100644 index 0000000..9f93d7e --- /dev/null +++ b/src/main/java/pages/ViewPage.java @@ -0,0 +1,67 @@ +package pages; + +import io.qameta.allure.Step; +import models.Ticket; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedCondition; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.testng.Assert; + +/** Страница просмотра карточки тикета (неавторизированный пользователь) */ +public class ViewPage extends HelpdeskBasePage { + + // Способ объявления и инициализации элементов страницы через driver.findElement(locator) + + /* Инициализация сразу при объявлении элемента. + Элемент должен присутствовать на странице браузера в момент создания объекта страницы new ViewPage() */ + private WebElement queue = driver.findElement(By.xpath("//th[contains(text(), 'Queue:')]")); + private WebElement email = driver.findElement(By.xpath("//th[text()='Submitter E-Mail']/following-sibling::td[1]")); + private WebElement priority = driver.findElement(By.xpath("//th[text()='Priority']/following-sibling::td[1]")); + + // Пример поиска description, используя промежуточный элемент descriptionLabel + private WebElement descriptionLabel = driver.findElement(By.xpath("//th[text()='Description']")); + // Поиск элемента можно выполнять не только относительно driver, но и относительно другого элемента + private WebElement description = descriptionLabel.findElement(By.xpath("./parent::*/following-sibling::tr[1]")); + + /* Поиск элемента по локатору. + Используется для элементов, которых нет на странице браузера в момент создания объекта страницы, + так как локатор может быть объявлен и проинициализирован до появления элемнета на странице. */ + private By captionLocator = By.xpath("//table/caption"); + private WebElement caption; + + + public ViewPage() { + // В данном случае инициализация через PageFactory не нужна, + // но можем проинициализировать элементы по локаторам (если элементы отображаются) + caption = driver.findElement(captionLocator); + } + + @Step("Проверить значение полей на карточке тикета") + public ViewPage checkTicket(Ticket ticket) { + Assert.assertTrue(getTicketTitle().contains(ticket.getTitle()), "Имя тикета не соответствует"); + // todo: добавить проверки + return this; + } + + @Step("Получить заголовок тикета") + public String getTicketTitle() { + // Если элемент появляется не сразу, можно выполнить ожидание по условию + + // условие видимости при поиске по локатору + ExpectedCondition condition = ExpectedConditions.visibilityOfElementLocated(captionLocator); + + // поиск с ожиданием по условию + WebElement ticketTitle = new WebDriverWait(driver, 5).until(condition); + + return ticketTitle.getText(); + } + + @Step("Сохранить id тикета в объект") + public void saveId(Ticket ticket) { + String captionText = caption.getText(); + String id = captionText.substring(captionText.indexOf("-") + 1, captionText.indexOf("]")); + ticket.setId(Integer.parseInt(id)); + } +} diff --git a/src/main/resources/config.properties b/src/main/resources/config.properties new file mode 100644 index 0000000..68c3d5a --- /dev/null +++ b/src/main/resources/config.properties @@ -0,0 +1,2 @@ +site.url= +webdriver.chrome.driver= \ No newline at end of file diff --git a/src/main/resources/user.properties b/src/main/resources/user.properties new file mode 100644 index 0000000..5c16481 --- /dev/null +++ b/src/main/resources/user.properties @@ -0,0 +1,2 @@ +user= +password= \ No newline at end of file diff --git a/src/test/java/web/HelpdeskUITest.java b/src/test/java/web/HelpdeskUITest.java new file mode 100644 index 0000000..830d709 --- /dev/null +++ b/src/test/java/web/HelpdeskUITest.java @@ -0,0 +1,69 @@ +package web; + +import io.qameta.allure.Step; +import models.Ticket; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import pages.AbstractPage; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public class HelpdeskUITest { + + private WebDriver driver; + private Ticket ticket; + + @BeforeClass + public void setup() throws IOException { + loadProperties(); + setupDriver(); + } + + @Step("Загрузить конфигурацилнные файлы") + private void loadProperties() throws IOException { + // Читаем конфигурационные файлы в System.properties + System.getProperties().load(ClassLoader.getSystemResourceAsStream("config.properties")); + System.getProperties().load(ClassLoader.getSystemResourceAsStream("user.properties")); + } + + @Step("Создать экземпляр драйвера") + private void setupDriver() { + // Создание экземпляра драйвера + driver = new ChromeDriver(); + // Устанавливаем размер окна браузера, как максимально возможный + driver.manage().window().maximize(); + // Установим время ожидания для поиска элементов + driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); + // Установить созданный драйвер для поиска в веб-страницах + AbstractPage.setDriver(driver); + } + + @Test + public void createTicketTest() { + // todo: шаги тест-кейса + + // ... + ticket = buildNewTicket(); + // ... + } + + private Ticket buildNewTicket() { + Ticket ticket = new Ticket(); + // todo: заполнить поля тикета + return ticket; + } + + @AfterTest + public void close() { + if (driver != null) { + // Закрываем одно текущее окно браузера + driver.close(); + // Закрываем все открытые окна браузера, завершаем работу браузера, освобождаем ресурсы + driver.quit(); + } + } +}