diff --git a/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsControllerTest.java b/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsControllerTest.java index 7f839c0..18438e9 100644 --- a/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsControllerTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsControllerTest.java @@ -20,6 +20,9 @@ import com.DesktopApplicationClientJava.services.ServiceRegistry; import com.DesktopApplicationClientJava.session.Session; import com.DesktopApplicationClientJava.utils.ControllerWiring; +import io.qameta.allure.Epic; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; import java.io.File; import java.nio.file.Path; import java.time.LocalDateTime; @@ -35,9 +38,14 @@ import javafx.stage.Window; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +@Epic("Ресурсы") +@Feature("№3 Просмотр метаданных ресурса") +@DisplayName("Детали ресурса") class ResourceDetailsControllerTest { private static final UUID RESOURCE_ID = UUID.randomUUID(); @@ -114,6 +122,8 @@ void setUp() { } @Test + @Tag("role:user") + @DisplayName("Загрузка показывает метаданные и файлы") void successfulLoadShowsMetadataAndFiles() { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); controller.setResourceId(RESOURCE_ID); @@ -129,6 +139,8 @@ void successfulLoadShowsMetadataAndFiles() { } @Test + @Tag("role:user") + @DisplayName("Отсутствующий ресурс: ошибка и пустая таблица") void resourceNotFoundShowsErrorAndLeavesTableEmpty() { when(resourceService.getById(RESOURCE_ID)).thenThrow(new ServiceException("Ресурс не найден")); controller.setResourceId(RESOURCE_ID); @@ -140,6 +152,9 @@ void resourceNotFoundShowsErrorAndLeavesTableEmpty() { } @Test + @Story("Скачивание") + @Tag("role:user") + @DisplayName("Скачивание сохраняет файл и показывает статус") void downloadSavesFileAndShowsStatus(@TempDir Path tempDir) { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); controller.setResourceId(RESOURCE_ID); @@ -156,6 +171,9 @@ void downloadSavesFileAndShowsStatus(@TempDir Path tempDir) { } @Test + @Story("Скачивание") + @Tag("role:user") + @DisplayName("Ошибка скачивания показывается в строке ошибки") void downloadErrorShowsInErrorLabel(@TempDir Path tempDir) { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); controller.setResourceId(RESOURCE_ID); @@ -172,6 +190,9 @@ void downloadErrorShowsInErrorLabel(@TempDir Path tempDir) { } @Test + @Story("Скачивание") + @Tag("role:user") + @DisplayName("Отмена выбора каталога не вызывает скачивание") void cancelledDirectoryChooserDoesNotCallDownload() { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); controller.setResourceId(RESOURCE_ID); @@ -185,12 +206,17 @@ void cancelledDirectoryChooserDoesNotCallDownload() { } @Test + @Tag("role:user") + @DisplayName("Кнопка «Назад» ведёт в список ресурсов") void backNavigatesToResourceList() { controller.onBack(new ActionEvent()); verify(navigator).goTo("/fxml/ResourceList.fxml"); } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Подтверждённое удаление вызывает сервис и ведёт в список") void confirmedDeleteCallsServiceAndNavigatesToList() { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); controller.setResourceId(RESOURCE_ID); @@ -204,6 +230,9 @@ void confirmedDeleteCallsServiceAndNavigatesToList() { } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Отменённое удаление не вызывает сервис") void cancelledDeleteDoesNotCallServiceNorNavigate() { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); controller.setResourceId(RESOURCE_ID); @@ -217,6 +246,9 @@ void cancelledDeleteDoesNotCallServiceNorNavigate() { } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Ошибка удаления показывается без перехода") void deleteErrorShowsInErrorLabelWithoutNavigation() { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); controller.setResourceId(RESOURCE_ID); diff --git a/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceFormControllerTest.java b/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceFormControllerTest.java index 6d99360..52191ad 100644 --- a/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceFormControllerTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/controllers/ResourceFormControllerTest.java @@ -17,6 +17,8 @@ import com.DesktopApplicationClientJava.services.ServiceRegistry; import com.DesktopApplicationClientJava.session.Session; import com.DesktopApplicationClientJava.utils.ControllerWiring; +import io.qameta.allure.Epic; +import io.qameta.allure.Feature; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -27,8 +29,14 @@ import javafx.scene.control.TextField; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +@Epic("Ресурсы") +@Feature("№4 Форма ресурса") +@Tag("role:moder") +@DisplayName("Форма ресурса") class ResourceFormControllerTest { private static final UUID RESOURCE_ID = UUID.randomUUID(); @@ -69,12 +77,14 @@ void setUp() { } @Test + @DisplayName("Режим создания показывает заголовок формы создания") void createModeShowsNewDocumentTitle() { assertEquals("Новый документ", controller.formTitleLabel.getText()); assertFalse(controller.errorLabel.isVisible()); } @Test + @DisplayName("Пустое название: ошибка без сохранения") void blankTitleShowsErrorAndDoesNotSave() { controller.onSave(new ActionEvent()); @@ -85,6 +95,7 @@ void blankTitleShowsErrorAndDoesNotSave() { } @Test + @DisplayName("Короткое название: ошибка без сохранения") void shortTitleShowsErrorAndDoesNotSave() { controller.titleField.setText("ab"); controller.onSave(new ActionEvent()); @@ -95,6 +106,7 @@ void shortTitleShowsErrorAndDoesNotSave() { } @Test + @DisplayName("Длинное название: ошибка без сохранения") void longTitleShowsErrorAndDoesNotSave() { controller.titleField.setText("a".repeat(256)); controller.onSave(new ActionEvent()); @@ -106,6 +118,7 @@ void longTitleShowsErrorAndDoesNotSave() { } @Test + @DisplayName("Корректный ввод создаёт ресурс и ведёт в список") void validInputCreatesAndNavigatesToList() { controller.titleField.setText(TITLE); controller.descriptionArea.setText(DESCRIPTION); @@ -117,6 +130,7 @@ void validInputCreatesAndNavigatesToList() { } @Test + @DisplayName("Пустое описание сохраняется как null") void blankDescriptionIsSavedAsNull() { controller.titleField.setText(TITLE); controller.onSave(new ActionEvent()); @@ -126,6 +140,7 @@ void blankDescriptionIsSavedAsNull() { } @Test + @DisplayName("Ошибка сервиса показывается без перехода") void serviceErrorShowsInErrorLabelAndDoesNotNavigate() { when(resourceService.create(anyString(), any())) .thenThrow(new ServiceException("Не удалось сохранить")); @@ -139,6 +154,7 @@ void serviceErrorShowsInErrorLabelAndDoesNotNavigate() { } @Test + @DisplayName("Отмена ведёт в список без сохранения") void cancelNavigatesToListWithoutSaving() { controller.titleField.setText(TITLE); controller.onCancel(new ActionEvent()); @@ -149,6 +165,7 @@ void cancelNavigatesToListWithoutSaving() { } @Test + @DisplayName("Режим редактирования подставляет сохранённые значения") void editModePrefillsExistingValues() { when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); controller.setResourceId(RESOURCE_ID); @@ -159,6 +176,7 @@ void editModePrefillsExistingValues() { } @Test + @DisplayName("Сохранение обновляет ресурс и ведёт в список") void editModeSaveUpdatesAndNavigatesToList() { when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); controller.setResourceId(RESOURCE_ID); @@ -172,6 +190,7 @@ void editModeSaveUpdatesAndNavigatesToList() { } @Test + @DisplayName("Ошибка загрузки показывает ошибку") void editModeLoadErrorShowsInErrorLabel() { when(resourceService.getById(RESOURCE_ID)).thenThrow(new ServiceException("Ресурс не найден")); controller.setResourceId(RESOURCE_ID); diff --git a/src/test/java/com/DesktopApplicationClientJava/ui/MockFlowTest.java b/src/test/java/com/DesktopApplicationClientJava/ui/MockFlowTest.java index cc26468..cb67199 100644 --- a/src/test/java/com/DesktopApplicationClientJava/ui/MockFlowTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/MockFlowTest.java @@ -28,8 +28,8 @@ * Mock-backed UI flow: real pages wired to real in-memory mock services (no Mockito on services, no * backend). Proves that MOCK_DATA mode renders actual data. */ -@Epic("Ресурс") -@Feature("Просмотр метаданных ресурса") +@Epic("Ресурсы") +@Feature("№3 Просмотр метаданных ресурса") @Tag("role:user") @DisplayName("Просмотр ресурса на моках") class MockFlowTest extends FxTestBase { @@ -72,6 +72,7 @@ void start(Stage stage) throws Exception { } @Test + @DisplayName("Мок-вход кладёт пользователя в сессию") void mockLoginPutsUserIntoSession() { Session session = new Session(); ServiceRegistry registry = ServiceRegistry.mock(session); @@ -99,6 +100,7 @@ void detailsPageRendersSeededMockResource() { @Test @SuppressWarnings("unchecked") + @DisplayName("Страница без id показывает первый мок-ресурс") void detailsPageAutoSelectsFirstSeedWithoutId() { robot.interact(() -> controller.load()); diff --git a/src/test/java/com/DesktopApplicationClientJava/ui/ResourceDetailsControllerUiTest.java b/src/test/java/com/DesktopApplicationClientJava/ui/ResourceDetailsControllerUiTest.java index a0ce2e2..22876b4 100644 --- a/src/test/java/com/DesktopApplicationClientJava/ui/ResourceDetailsControllerUiTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/ResourceDetailsControllerUiTest.java @@ -21,6 +21,9 @@ import com.DesktopApplicationClientJava.services.ServiceRegistry; import com.DesktopApplicationClientJava.session.Session; import com.DesktopApplicationClientJava.utils.ControllerWiring; +import io.qameta.allure.Epic; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -35,6 +38,8 @@ import javafx.scene.control.Label; import javafx.stage.Stage; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.testfx.api.FxRobot; import org.testfx.api.FxToolkit; @@ -44,6 +49,9 @@ * Компонентные тесты страницы деталей ресурса: удаление через настоящую модалку подтверждения (роль * МОДЕР). */ +@Epic("Ресурсы") +@Feature("№3 Просмотр метаданных ресурса") +@DisplayName("Детали ресурса: удаление через модалку") class ResourceDetailsControllerUiTest extends FxTestBase { private static final UUID RESOURCE_ID = UUID.randomUUID(); @@ -115,12 +123,16 @@ private void loadPage() { } @Test + @Tag("role:moder") + @DisplayName("Модер видит кнопки редактирования и удаления") void moderatorSeesEditAndDeleteButtons() { assertTrue(robot.lookup("#editButton").queryAs(Button.class).isVisible()); assertTrue(robot.lookup("#deleteButton").queryAs(Button.class).isVisible()); } @Test + @Tag("role:user") + @DisplayName("Пользователь не видит кнопок редактирования и удаления") void plainUserSeesNeitherEditNorDeleteButtons() { robot.interact( () -> { @@ -138,6 +150,9 @@ void plainUserSeesNeitherEditNorDeleteButtons() { } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Клик модера открывает модалку подтверждения") void moderatorDeleteClickOpensConfirmationModal() { robot.interact( () -> { @@ -162,6 +177,9 @@ void moderatorDeleteClickOpensConfirmationModal() { } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Подтверждение удаляет ресурс и ведёт в список") void confirmingModalClosesItDeletesAndNavigatesToList() { robot.interact( () -> { @@ -180,6 +198,9 @@ void confirmingModalClosesItDeletesAndNavigatesToList() { } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Удаление отсутствующего ресурса показывает ошибку без перехода") void confirmingDeleteOfGoneResourceShowsErrorWithoutNavigation() { robot.interact( () -> { diff --git a/src/test/java/com/DesktopApplicationClientJava/ui/ResourceListControllerTest.java b/src/test/java/com/DesktopApplicationClientJava/ui/ResourceListControllerTest.java index 4a048f3..6a9dc6f 100644 --- a/src/test/java/com/DesktopApplicationClientJava/ui/ResourceListControllerTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/ResourceListControllerTest.java @@ -27,6 +27,9 @@ import com.DesktopApplicationClientJava.services.filter.ResourceFilter; import com.DesktopApplicationClientJava.session.Session; import com.DesktopApplicationClientJava.utils.ControllerWiring; +import io.qameta.allure.Epic; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -45,11 +48,16 @@ import javafx.scene.control.TextField; import javafx.stage.Stage; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.testfx.api.FxRobot; import org.testfx.api.FxToolkit; import org.testfx.util.WaitForAsyncUtils; +@Epic("Ресурсы") +@Feature("№2 Список ресурсов") +@DisplayName("Список ресурсов") class ResourceListControllerTest extends FxTestBase { private static final User ADMIN = @@ -125,11 +133,15 @@ private void loadPage() { } @Test + @Tag("role:user") + @DisplayName("Шапка показывает текущего пользователя") void showsCurrentUserInHeader() { assertEquals("Имя Фамилия · ADMIN", textOf(robot, "#userInfoLabel")); } @Test + @Tag("role:moder") + @DisplayName("Администратор видит кнопку создания") void adminSeesCreateButton() { Button create = robot.lookup("#createButton").queryAs(Button.class); assertTrue(create.isVisible()); @@ -137,6 +149,8 @@ void adminSeesCreateButton() { } @Test + @Tag("role:moder") + @DisplayName("Модер видит кнопку создания") void moderatorSeesCreateButton() { robot.interact( () -> { @@ -149,6 +163,8 @@ void moderatorSeesCreateButton() { } @Test + @Tag("role:user") + @DisplayName("Пользователь не видит кнопку создания") void plainUserDoesNotSeeCreateButton() { robot.interact( () -> { @@ -163,6 +179,8 @@ void plainUserDoesNotSeeCreateButton() { } @Test + @Tag("role:user") + @DisplayName("Открытие страницы сразу грузит первую страницу") void initialOpenLoadsFirstPageWithoutInteraction() { // Regression test for the missing auto-load: opening the page must fetch the first page // with no user interaction. Red until initialize() triggers the initial load. @@ -177,6 +195,9 @@ void initialOpenLoadsFirstPageWithoutInteraction() { } @Test + @Story("Поиск/сортировка") + @Tag("role:user") + @DisplayName("Сортировка по умолчанию — сначала новые") void sortDefaultsToNewestFirst() { @SuppressWarnings("unchecked") ComboBox sort = robot.lookup("#sortCombo").queryAs(ComboBox.class); @@ -187,6 +208,9 @@ void sortDefaultsToNewestFirst() { } @Test + @Story("Поиск/сортировка") + @Tag("role:user") + @DisplayName("Поиск подгружает подходящие строки") void searchLoadsMatchingRows() { when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) .thenReturn(List.of(resource("Документ 1"))); @@ -208,6 +232,9 @@ void searchLoadsMatchingRows() { } @Test + @Story("Поиск/сортировка") + @Tag("role:user") + @DisplayName("Смена сортировки перезагружает по названию") void changingSortReloadsWithTitleAsc() { when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) .thenReturn(List.of()); @@ -223,6 +250,8 @@ void changingSortReloadsWithTitleAsc() { } @Test + @Tag("role:user") + @DisplayName("Следующая страница грузится со смещением") void nextPageLoadsWithOffsetAndDisablesButtonsOnShortPage() { when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) .thenReturn(List.of(resource("Документ 1"))); @@ -247,6 +276,8 @@ void nextPageLoadsWithOffsetAndDisablesButtonsOnShortPage() { } @Test + @Tag("role:user") + @DisplayName("Ошибка сервиса показывает ошибку и очищает таблицу") void serviceErrorShowsErrorAndClearsTable() { when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) .thenThrow(new ServiceException("Сервер недоступен")); @@ -262,6 +293,8 @@ void serviceErrorShowsErrorAndClearsTable() { } @Test + @Tag("role:user") + @DisplayName("Выход вызывает сервис и ведёт на страницу входа") void logoutCallsServiceAndNavigatesToLogin() { robot.clickOn("#logoutButton"); @@ -270,6 +303,8 @@ void logoutCallsServiceAndNavigatesToLogin() { } @Test + @Tag("role:user") + @DisplayName("Двойной клик по строке открывает детали") void doubleClickRowOpensDetails() { Resource resource = resource("Документ 1"); when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) @@ -283,6 +318,9 @@ void doubleClickRowOpensDetails() { } @Test + @Story("Поиск/сортировка") + @Tag("role:user") + @DisplayName("Поиск отправляет фильтры названия и описания") void searchSendsTitleAndDescriptionFilters() { when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) .thenReturn(List.of(resource("Документ 1"))); @@ -306,6 +344,9 @@ void searchSendsTitleAndDescriptionFilters() { } @Test + @Story("Поиск/сортировка") + @Tag("role:user") + @DisplayName("Пустое описание отправляется как null-фильтр") void blankDescriptionSendsNullFilter() { when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) .thenReturn(List.of(resource("Документ 1"))); @@ -323,6 +364,8 @@ void blankDescriptionSendsNullFilter() { } @Test + @Tag("role:user") + @DisplayName("Пользователь не видит кнопку удаления") void deleteButtonHiddenFromPlainUser() { robot.interact( () -> { @@ -337,6 +380,8 @@ void deleteButtonHiddenFromPlainUser() { } @Test + @Tag("role:moder") + @DisplayName("Без выбранной строки удаление недоступно модеру") void deleteButtonDisabledWithoutSelectionForModerator() { robot.interact( () -> { @@ -351,6 +396,8 @@ void deleteButtonDisabledWithoutSelectionForModerator() { } @Test + @Tag("role:moder") + @DisplayName("Выбор строки включает кнопку удаления") void selectingRowEnablesDeleteButton() { Resource resource = resource("Документ 1"); when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) @@ -364,6 +411,9 @@ void selectingRowEnablesDeleteButton() { } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Подтверждённое удаление вызывает сервис и обновляет список") void confirmedDeleteCallsServiceAndReloads() { Resource resource = resource("Документ 1"); when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) @@ -381,6 +431,9 @@ void confirmedDeleteCallsServiceAndReloads() { } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Отменённое удаление не вызывает сервис") void cancelledDeleteDoesNotCallService() { Resource resource = resource("Документ 1"); when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) @@ -395,6 +448,9 @@ void cancelledDeleteDoesNotCallService() { } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Ошибка удаления показывается в строке ошибки") void deleteErrorShowsErrorLabel() { Resource resource = resource("Документ 1"); when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) @@ -414,6 +470,9 @@ void deleteErrorShowsErrorLabel() { } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Клик модера открывает модалку подтверждения") void moderatorDeleteClickOpensConfirmationModal() { givenModeratorPageWithResource(resource("Документ 1")); @@ -435,6 +494,9 @@ void moderatorDeleteClickOpensConfirmationModal() { } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Подтверждение в модалке закрывает её и удаляет ресурс") void confirmingModalClosesItAndDeletesResource() { Resource resource = resource("Документ 1"); givenModeratorPageWithResource(resource); @@ -451,6 +513,9 @@ void confirmingModalClosesItAndDeletesResource() { } @Test + @Story("Удаление с подтверждением") + @Tag("role:moder") + @DisplayName("Удаление отсутствующего ресурса показывает ошибку") void deletingGoneResourceShowsErrorThroughModal() { Resource resource = resource("Документ 1"); when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString()))