diff --git a/src/test/java/ui/FxTestBase.java b/src/test/java/ui/FxTestBase.java new file mode 100644 index 0000000..f46c6cb --- /dev/null +++ b/src/test/java/ui/FxTestBase.java @@ -0,0 +1,52 @@ +package ui; + +import java.util.concurrent.TimeoutException; +import javafx.scene.control.TextInputControl; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.testfx.api.FxRobot; +import org.testfx.api.FxToolkit; + +/** + * Toolkit поднимается один раз на класс, а не перед каждым тестом, как это делает {@code + * ApplicationExtension}. Чистое состояние между тестами даёт пересоздание сцены и моков в {@code + * BeforeEach} наследника. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class FxTestBase { + + protected final FxRobot robot = new FxRobot(); + + @BeforeAll + void initToolkit() throws TimeoutException { + startSharedToolkit(); + } + + @AfterAll + void cleanupToolkit() throws TimeoutException { + stopSharedToolkit(robot); + } + + /** + * Подъём toolkit для тестов, которые не могут наследовать этот класс (уже есть предок). Повторные + * вызовы дешёвые: toolkit стартует один раз на JVM. + */ + public static void startSharedToolkit() throws TimeoutException { + FxToolkit.registerPrimaryStage(); + } + + /** Пара к {@link #startSharedToolkit()}. */ + public static void stopSharedToolkit(FxRobot robot) throws TimeoutException { + FxToolkit.cleanupStages(); + FxToolkit.cleanupInput(robot); + } + + /** + * Синтетический ввод под headless Monocle идёт ~90 мс на клавишу и иногда теряется, а контроллеры + * читают поля только по кнопке, поэтому текст ставится напрямую. + */ + protected static void fillText(FxRobot robot, String query, String value) { + robot.interact(() -> robot.lookup(query).queryAs(TextInputControl.class).setText(value)); + } +} diff --git a/src/test/java/ui/LoginFormControllerTest.java b/src/test/java/ui/LoginFormControllerTest.java index 6ef872f..2aed3df 100644 --- a/src/test/java/ui/LoginFormControllerTest.java +++ b/src/test/java/ui/LoginFormControllerTest.java @@ -23,14 +23,11 @@ import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.stage.Stage; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.testfx.api.FxRobot; -import org.testfx.framework.junit5.ApplicationExtension; -import org.testfx.framework.junit5.Start; +import org.testfx.api.FxToolkit; -@ExtendWith(ApplicationExtension.class) -class LoginFormControllerTest { +class LoginFormControllerTest extends FxTestBase { private static final String VALID_EMAIL = "user@example.com"; private static final String VALID_PASSWORD = "secret123"; @@ -38,7 +35,18 @@ class LoginFormControllerTest { private AuthService authService; private Navigator navigator; - @Start + @BeforeEach + void resetScene() throws Exception { + FxToolkit.setupStage( + stage -> { + try { + start(stage); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + void start(Stage stage) throws Exception { authService = mock(AuthService.class); ServiceRegistry services = mock(ServiceRegistry.class); @@ -62,7 +70,7 @@ void start(Stage stage) throws Exception { } @Test - void emptyFieldsShowErrorAndDoNotCallApi(FxRobot robot) { + void emptyFieldsShowErrorAndDoNotCallApi() { robot.clickOn("#submitButton"); Label error = robot.lookup("#errorLabel").queryAs(Label.class); @@ -73,8 +81,8 @@ void emptyFieldsShowErrorAndDoNotCallApi(FxRobot robot) { } @Test - void blankPasswordShowsErrorAndDoesNotCallApi(FxRobot robot) { - robot.clickOn("#emailField").write(VALID_EMAIL); + void blankPasswordShowsErrorAndDoesNotCallApi() { + fillText(robot, "#emailField", VALID_EMAIL); robot.clickOn("#submitButton"); Label error = robot.lookup("#errorLabel").queryAs(Label.class); @@ -85,9 +93,9 @@ void blankPasswordShowsErrorAndDoesNotCallApi(FxRobot robot) { } @Test - void invalidEmailShowsErrorAndDoesNotCallApi(FxRobot robot) { - robot.clickOn("#emailField").write("not-an-email"); - robot.clickOn("#passwordField").write(VALID_PASSWORD); + void invalidEmailShowsErrorAndDoesNotCallApi() { + fillText(robot, "#emailField", "not-an-email"); + fillText(robot, "#passwordField", VALID_PASSWORD); robot.clickOn("#submitButton"); Label error = robot.lookup("#errorLabel").queryAs(Label.class); @@ -98,12 +106,12 @@ void invalidEmailShowsErrorAndDoesNotCallApi(FxRobot robot) { } @Test - void successfulLoginNavigatesToResourceList(FxRobot robot) { + void successfulLoginNavigatesToResourceList() { User user = new User(UUID.randomUUID(), VALID_EMAIL, Role.USER, "Имя", "Фамилия", true); when(authService.login(VALID_EMAIL, VALID_PASSWORD)).thenReturn(user); - robot.clickOn("#emailField").write(VALID_EMAIL); - robot.clickOn("#passwordField").write(VALID_PASSWORD); + fillText(robot, "#emailField", VALID_EMAIL); + fillText(robot, "#passwordField", VALID_PASSWORD); robot.clickOn("#submitButton"); verify(authService).login(VALID_EMAIL, VALID_PASSWORD); @@ -113,12 +121,12 @@ void successfulLoginNavigatesToResourceList(FxRobot robot) { } @Test - void failedLoginShowsServerErrorAndDoesNotNavigate(FxRobot robot) { + void failedLoginShowsServerErrorAndDoesNotNavigate() { when(authService.login(VALID_EMAIL, VALID_PASSWORD)) .thenThrow(new ServiceException("Неверный логин или пароль")); - robot.clickOn("#emailField").write(VALID_EMAIL); - robot.clickOn("#passwordField").write(VALID_PASSWORD); + fillText(robot, "#emailField", VALID_EMAIL); + fillText(robot, "#passwordField", VALID_PASSWORD); robot.clickOn("#submitButton"); Label error = robot.lookup("#errorLabel").queryAs(Label.class); diff --git a/src/test/java/ui/MockFlowTest.java b/src/test/java/ui/MockFlowTest.java index 55d112d..1df371c 100644 --- a/src/test/java/ui/MockFlowTest.java +++ b/src/test/java/ui/MockFlowTest.java @@ -16,25 +16,33 @@ import javafx.scene.control.Label; import javafx.scene.control.TableView; import javafx.stage.Stage; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.testfx.api.FxRobot; -import org.testfx.framework.junit5.ApplicationExtension; -import org.testfx.framework.junit5.Start; +import org.testfx.api.FxToolkit; /** * 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. */ -@ExtendWith(ApplicationExtension.class) -class MockFlowTest { +class MockFlowTest extends FxTestBase { private static final UUID SEEDED_RESOURCE_ID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); private ResourceDetailsController controller; - @Start + @BeforeEach + void resetScene() throws Exception { + FxToolkit.setupStage( + stage -> { + try { + start(stage); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + void start(Stage stage) throws Exception { Navigator navigator = mockNavigator(); ServiceRegistry services = ServiceRegistry.mock(new Session()); @@ -68,7 +76,7 @@ void mockLoginPutsUserIntoSession() { @Test @SuppressWarnings("unchecked") - void detailsPageRendersSeededMockResource(FxRobot robot) { + void detailsPageRendersSeededMockResource() { robot.interact( () -> { controller.setResourceId(SEEDED_RESOURCE_ID); @@ -83,7 +91,7 @@ void detailsPageRendersSeededMockResource(FxRobot robot) { @Test @SuppressWarnings("unchecked") - void detailsPageAutoSelectsFirstSeedWithoutId(FxRobot robot) { + void detailsPageAutoSelectsFirstSeedWithoutId() { robot.interact(() -> controller.load()); assertEquals("Годовой отчёт (мок)", robot.lookup("#titleLabel").queryAs(Label.class).getText()); diff --git a/src/test/java/ui/ResourceDetailsControllerTest.java b/src/test/java/ui/ResourceDetailsControllerTest.java index 940e75c..f25c083 100644 --- a/src/test/java/ui/ResourceDetailsControllerTest.java +++ b/src/test/java/ui/ResourceDetailsControllerTest.java @@ -32,15 +32,13 @@ import javafx.scene.control.TableView; import javafx.stage.Stage; import javafx.stage.Window; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; import org.testfx.api.FxRobot; -import org.testfx.framework.junit5.ApplicationExtension; -import org.testfx.framework.junit5.Start; +import org.testfx.api.FxToolkit; -@ExtendWith(ApplicationExtension.class) -class ResourceDetailsControllerTest { +class ResourceDetailsControllerTest extends FxTestBase { private static final UUID RESOURCE_ID = UUID.randomUUID(); private static final UUID FILE_ID_1 = UUID.randomUUID(); @@ -64,7 +62,18 @@ protected File chooseDirectory(Window owner) { } } - @Start + @BeforeEach + void resetScene() throws Exception { + FxToolkit.setupStage( + stage -> { + try { + start(stage); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + void start(Stage stage) throws Exception { resourceService = mock(ResourceService.class); fileService = mock(FileService.class); @@ -83,7 +92,7 @@ void start(Stage stage) throws Exception { } @Test - void successfulLoadShowsMetadataAndFiles(FxRobot robot) { + void successfulLoadShowsMetadataAndFiles() { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); loadPage(robot); @@ -101,7 +110,7 @@ void successfulLoadShowsMetadataAndFiles(FxRobot robot) { } @Test - void resourceNotFoundShowsErrorAndLeavesTableEmpty(FxRobot robot) { + void resourceNotFoundShowsErrorAndLeavesTableEmpty() { when(resourceService.getById(RESOURCE_ID)).thenThrow(new ServiceException("Ресурс не найден")); loadPage(robot); @@ -113,7 +122,7 @@ void resourceNotFoundShowsErrorAndLeavesTableEmpty(FxRobot robot) { } @Test - void downloadSavesFileAndShowsStatus(FxRobot robot, @TempDir Path tempDir) { + void downloadSavesFileAndShowsStatus(@TempDir Path tempDir) { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); loadPage(robot); @@ -128,7 +137,7 @@ void downloadSavesFileAndShowsStatus(FxRobot robot, @TempDir Path tempDir) { } @Test - void downloadErrorShowsInErrorLabel(FxRobot robot, @TempDir Path tempDir) { + void downloadErrorShowsInErrorLabel(@TempDir Path tempDir) { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); loadPage(robot); @@ -144,7 +153,7 @@ void downloadErrorShowsInErrorLabel(FxRobot robot, @TempDir Path tempDir) { } @Test - void cancelledDirectoryChooserDoesNotCallDownload(FxRobot robot) { + void cancelledDirectoryChooserDoesNotCallDownload() { when(resourceService.getById(RESOURCE_ID)).thenReturn(resourceWithFiles()); loadPage(robot); @@ -155,7 +164,7 @@ void cancelledDirectoryChooserDoesNotCallDownload(FxRobot robot) { } @Test - void backNavigatesToResourceList(FxRobot robot) { + void backNavigatesToResourceList() { robot.clickOn("#backButton"); verify(navigator).goTo("/fxml/ResourceList.fxml"); diff --git a/src/test/java/ui/ResourceFormControllerTest.java b/src/test/java/ui/ResourceFormControllerTest.java index bd50698..008a423 100644 --- a/src/test/java/ui/ResourceFormControllerTest.java +++ b/src/test/java/ui/ResourceFormControllerTest.java @@ -28,14 +28,12 @@ import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.stage.Stage; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.testfx.api.FxRobot; -import org.testfx.framework.junit5.ApplicationExtension; -import org.testfx.framework.junit5.Start; +import org.testfx.api.FxToolkit; -@ExtendWith(ApplicationExtension.class) -class ResourceFormControllerTest { +class ResourceFormControllerTest extends FxTestBase { private static final UUID RESOURCE_ID = UUID.randomUUID(); private static final String TITLE = "Годовой отчёт"; @@ -47,7 +45,18 @@ class ResourceFormControllerTest { private ResourceFormController controller; private Stage stage; - @Start + @BeforeEach + void resetScene() throws Exception { + FxToolkit.setupStage( + stage -> { + try { + start(stage); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + void start(Stage stage) throws Exception { resourceService = mock(ResourceService.class); services = mock(ServiceRegistry.class); @@ -82,13 +91,13 @@ private void openEditForm(FxRobot robot, UUID resourceId) { } @Test - void createModeShowsNewDocumentTitle(FxRobot robot) { + void createModeShowsNewDocumentTitle() { assertEquals("Новый документ", textOf(robot, "#formTitleLabel")); assertFalse(robot.lookup("#errorLabel").queryAs(Label.class).isVisible()); } @Test - void blankTitleShowsErrorAndDoesNotSave(FxRobot robot) { + void blankTitleShowsErrorAndDoesNotSave() { robot.clickOn("#saveButton"); assertError(robot, "Название обязательно для заполнения"); @@ -98,8 +107,8 @@ void blankTitleShowsErrorAndDoesNotSave(FxRobot robot) { } @Test - void shortTitleShowsErrorAndDoesNotSave(FxRobot robot) { - robot.clickOn("#titleField").write("ab"); + void shortTitleShowsErrorAndDoesNotSave() { + fillText(robot, "#titleField", "ab"); robot.clickOn("#saveButton"); assertError(robot, "Название не может быть короче 3 символов"); @@ -108,8 +117,8 @@ void shortTitleShowsErrorAndDoesNotSave(FxRobot robot) { } @Test - void longTitleShowsErrorAndDoesNotSave(FxRobot robot) { - robot.clickOn("#titleField").write("a".repeat(256)); + void longTitleShowsErrorAndDoesNotSave() { + fillText(robot, "#titleField", "a".repeat(256)); robot.clickOn("#saveButton"); assertError(robot, "Название не может быть длиннее 255 символов"); @@ -119,9 +128,9 @@ void longTitleShowsErrorAndDoesNotSave(FxRobot robot) { } @Test - void validInputCreatesAndNavigatesToList(FxRobot robot) { - robot.clickOn("#titleField").write(TITLE); - robot.clickOn("#descriptionArea").write(DESCRIPTION); + void validInputCreatesAndNavigatesToList() { + fillText(robot, "#titleField", TITLE); + fillText(robot, "#descriptionArea", DESCRIPTION); robot.clickOn("#saveButton"); verify(resourceService).create(TITLE, DESCRIPTION); @@ -130,8 +139,8 @@ void validInputCreatesAndNavigatesToList(FxRobot robot) { } @Test - void blankDescriptionIsSavedAsNull(FxRobot robot) { - robot.clickOn("#titleField").write(TITLE); + void blankDescriptionIsSavedAsNull() { + fillText(robot, "#titleField", TITLE); robot.clickOn("#saveButton"); verify(resourceService).create(TITLE, null); @@ -139,11 +148,11 @@ void blankDescriptionIsSavedAsNull(FxRobot robot) { } @Test - void serviceErrorShowsInErrorLabelAndDoesNotNavigate(FxRobot robot) { + void serviceErrorShowsInErrorLabelAndDoesNotNavigate() { when(resourceService.create(anyString(), any())) .thenThrow(new ServiceException("Не удалось сохранить")); - robot.clickOn("#titleField").write(TITLE); + fillText(robot, "#titleField", TITLE); robot.clickOn("#saveButton"); assertError(robot, "Не удалось сохранить"); @@ -151,8 +160,8 @@ void serviceErrorShowsInErrorLabelAndDoesNotNavigate(FxRobot robot) { } @Test - void cancelNavigatesToListWithoutSaving(FxRobot robot) { - robot.clickOn("#titleField").write(TITLE); + void cancelNavigatesToListWithoutSaving() { + fillText(robot, "#titleField", TITLE); robot.clickOn("#cancelButton"); verify(navigator).goTo("/fxml/ResourceList.fxml"); @@ -161,7 +170,7 @@ void cancelNavigatesToListWithoutSaving(FxRobot robot) { } @Test - void editModePrefillsExistingValues(FxRobot robot) { + void editModePrefillsExistingValues() { when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); openEditForm(robot, RESOURCE_ID); @@ -171,7 +180,7 @@ void editModePrefillsExistingValues(FxRobot robot) { } @Test - void editModeSaveUpdatesAndNavigatesToList(FxRobot robot) { + void editModeSaveUpdatesAndNavigatesToList() { when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); openEditForm(robot, RESOURCE_ID); @@ -185,7 +194,7 @@ void editModeSaveUpdatesAndNavigatesToList(FxRobot robot) { } @Test - void editModeLoadErrorShowsInErrorLabel(FxRobot robot) { + void editModeLoadErrorShowsInErrorLabel() { when(resourceService.getById(RESOURCE_ID)).thenThrow(new ServiceException("Ресурс не найден")); openEditForm(robot, RESOURCE_ID); diff --git a/src/test/java/ui/ResourceListControllerTest.java b/src/test/java/ui/ResourceListControllerTest.java index 84b6470..af8fe27 100644 --- a/src/test/java/ui/ResourceListControllerTest.java +++ b/src/test/java/ui/ResourceListControllerTest.java @@ -39,15 +39,13 @@ import javafx.scene.control.TableView; import javafx.scene.control.TextField; import javafx.stage.Stage; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.testfx.api.FxRobot; -import org.testfx.framework.junit5.ApplicationExtension; -import org.testfx.framework.junit5.Start; +import org.testfx.api.FxToolkit; import org.testfx.util.WaitForAsyncUtils; -@ExtendWith(ApplicationExtension.class) -class ResourceListControllerTest { +class ResourceListControllerTest extends FxTestBase { private static final User ADMIN = new User(UUID.randomUUID(), "admin@example.com", Role.ADMIN, "Имя", "Фамилия", true); @@ -59,7 +57,18 @@ class ResourceListControllerTest { private ResourceListController controller; private Stage stage; - @Start + @BeforeEach + void resetScene() throws Exception { + FxToolkit.setupStage( + stage -> { + try { + start(stage); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + void start(Stage stage) throws Exception { authService = mock(AuthService.class); resourceService = mock(ResourceService.class); @@ -96,19 +105,19 @@ private void loadPage() { } @Test - void showsCurrentUserInHeader(FxRobot robot) { + void showsCurrentUserInHeader() { assertEquals("Имя Фамилия · ADMIN", textOf(robot, "#userInfoLabel")); } @Test - void adminSeesCreateButton(FxRobot robot) { + void adminSeesCreateButton() { Button create = robot.lookup("#createButton").queryAs(Button.class); assertTrue(create.isVisible()); assertTrue(create.isManaged()); } @Test - void moderatorSeesCreateButton(FxRobot robot) { + void moderatorSeesCreateButton() { robot.interact( () -> { when(authService.isAdmin()).thenReturn(false); @@ -120,7 +129,7 @@ void moderatorSeesCreateButton(FxRobot robot) { } @Test - void plainUserDoesNotSeeCreateButton(FxRobot robot) { + void plainUserDoesNotSeeCreateButton() { robot.interact( () -> { when(authService.isAdmin()).thenReturn(false); @@ -134,7 +143,7 @@ void plainUserDoesNotSeeCreateButton(FxRobot robot) { } @Test - void initialOpenLoadsFirstPageWithoutInteraction(FxRobot robot) { + 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. when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) @@ -148,7 +157,7 @@ void initialOpenLoadsFirstPageWithoutInteraction(FxRobot robot) { } @Test - void sortDefaultsToNewestFirst(FxRobot robot) { + void sortDefaultsToNewestFirst() { ComboBox sort = robot.lookup("#sortCombo").queryAs(ComboBox.class); assertEquals( @@ -158,7 +167,7 @@ void sortDefaultsToNewestFirst(FxRobot robot) { } @Test - void searchLoadsMatchingRows(FxRobot robot) { + void searchLoadsMatchingRows() { when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) .thenReturn(List.of(resource("Документ 1"))); @@ -179,7 +188,7 @@ void searchLoadsMatchingRows(FxRobot robot) { } @Test - void changingSortReloadsWithTitleAsc(FxRobot robot) { + void changingSortReloadsWithTitleAsc() { when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) .thenReturn(List.of()); @@ -194,7 +203,7 @@ void changingSortReloadsWithTitleAsc(FxRobot robot) { } @Test - void nextPageLoadsWithOffsetAndDisablesButtonsOnShortPage(FxRobot robot) { + void nextPageLoadsWithOffsetAndDisablesButtonsOnShortPage() { when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) .thenReturn(List.of(resource("Документ 1"))); @@ -218,7 +227,7 @@ void nextPageLoadsWithOffsetAndDisablesButtonsOnShortPage(FxRobot robot) { } @Test - void serviceErrorShowsErrorAndClearsTable(FxRobot robot) { + void serviceErrorShowsErrorAndClearsTable() { when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) .thenThrow(new ServiceException("Сервер недоступен")); @@ -233,7 +242,7 @@ void serviceErrorShowsErrorAndClearsTable(FxRobot robot) { } @Test - void logoutCallsServiceAndNavigatesToLogin(FxRobot robot) { + void logoutCallsServiceAndNavigatesToLogin() { robot.clickOn("#logoutButton"); verify(authService).logout(); @@ -241,7 +250,7 @@ void logoutCallsServiceAndNavigatesToLogin(FxRobot robot) { } @Test - void doubleClickRowOpensDetails(FxRobot robot) { + void doubleClickRowOpensDetails() { Resource resource = resource("Документ 1"); when(resourceService.search(any(), anyLong(), anyLong(), anyString(), anyString())) .thenReturn(List.of(resource)); diff --git a/src/test/java/ui/integration/ResourceFormIntegrationTest.java b/src/test/java/ui/integration/ResourceFormIntegrationTest.java index 53b698b..5c439cf 100644 --- a/src/test/java/ui/integration/ResourceFormIntegrationTest.java +++ b/src/test/java/ui/integration/ResourceFormIntegrationTest.java @@ -31,12 +31,13 @@ import javafx.scene.control.TextField; import javafx.scene.layout.StackPane; import javafx.stage.Stage; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.testfx.api.FxRobot; -import org.testfx.framework.junit5.ApplicationExtension; -import org.testfx.framework.junit5.Start; +import org.testfx.api.FxToolkit; +import ui.FxTestBase; /** * Integration tests for the resource create/edit flow: real services with a real ApiClient against @@ -51,15 +52,36 @@ * excludedGroups} in pom.xml). Run explicitly: {@code mvn test -Dgroups=integration * -DexcludedGroups= -Dtest=ResourceFormIntegrationTest}. */ -@ExtendWith(ApplicationExtension.class) class ResourceFormIntegrationTest extends IntegrationTestCase { + private final FxRobot robot = new FxRobot(); private Navigator navigator; private Stage stage; private Session session; private ServiceRegistry services; - @Start + @BeforeAll + static void initToolkit() throws Exception { + FxTestBase.startSharedToolkit(); + } + + @AfterAll + static void cleanupToolkit() throws Exception { + FxTestBase.stopSharedToolkit(new FxRobot()); + } + + @BeforeEach + void resetScene() throws Exception { + FxToolkit.setupStage( + stage -> { + try { + start(stage); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + void start(Stage stage) { this.stage = stage; this.navigator = mock(Navigator.class); @@ -146,7 +168,7 @@ void updateUnknownIdReturns404() { } @Test - void saveWithInvalidTokensShowsErrorAndDoesNotNavigate(FxRobot robot) { + void saveWithInvalidTokensShowsErrorAndDoesNotNavigate() { // Garbage tokens (not a fresh session) force a real HTTP 401 round-trip instead of the // client's offline "no refresh token" shortcut, proving server-side auth enforcement. Session poisoned = new Session(); diff --git a/src/test/java/ui/integration/ResourceListIntegrationTest.java b/src/test/java/ui/integration/ResourceListIntegrationTest.java index bc2f2c3..5ddb7b8 100644 --- a/src/test/java/ui/integration/ResourceListIntegrationTest.java +++ b/src/test/java/ui/integration/ResourceListIntegrationTest.java @@ -30,13 +30,14 @@ import javafx.scene.control.TextField; import javafx.scene.layout.StackPane; import javafx.stage.Stage; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.testfx.api.FxRobot; -import org.testfx.framework.junit5.ApplicationExtension; -import org.testfx.framework.junit5.Start; +import org.testfx.api.FxToolkit; import org.testfx.util.WaitForAsyncUtils; +import ui.FxTestBase; /** * UI-level integration tests for the resource list page: real services with a real ApiClient @@ -47,15 +48,36 @@ * the default build and CI (see surefire {@code excludedGroups} in pom.xml). Run explicitly: {@code * mvn test -Dgroups=integration -DexcludedGroups= -Dtest=ResourceListIntegrationTest}. */ -@ExtendWith(ApplicationExtension.class) class ResourceListIntegrationTest extends IntegrationTestCase { + private final FxRobot robot = new FxRobot(); private Navigator navigator; private Stage stage; private Session session; private ServiceRegistry services; - @Start + @BeforeAll + static void initToolkit() throws Exception { + FxTestBase.startSharedToolkit(); + } + + @AfterAll + static void cleanupToolkit() throws Exception { + FxTestBase.stopSharedToolkit(new FxRobot()); + } + + @BeforeEach + void resetScene() throws Exception { + FxToolkit.setupStage( + stage -> { + try { + start(stage); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + void start(Stage stage) { this.stage = stage; this.navigator = mock(Navigator.class); @@ -75,7 +97,7 @@ void setUpServices() { } @Test - void emptySearchShowsPlaceholderAndZeroCounter(FxRobot robot) { + void emptySearchShowsPlaceholderAndZeroCounter() { loginAsAdmin(); loadListPage(robot); @@ -88,7 +110,7 @@ void emptySearchShowsPlaceholderAndZeroCounter(FxRobot robot) { } @Test - void seededResourceAppearsInSearch(FxRobot robot) { + void seededResourceAppearsInSearch() { loginAsAdmin(); String title = "IT ResourceList " + UUID.randomUUID(); Resource created = resourceService.create(title, "created by ResourceListIntegrationTest"); @@ -108,7 +130,7 @@ void seededResourceAppearsInSearch(FxRobot robot) { } @Test - void sortByTitleAscOrdersSeededResources(FxRobot robot) { + void sortByTitleAscOrdersSeededResources() { loginAsAdmin(); String prefix = "IT ResourceSort " + UUID.randomUUID() + " "; Resource second = resourceService.create(prefix + "ZZZ", "seeded second"); @@ -133,7 +155,7 @@ void sortByTitleAscOrdersSeededResources(FxRobot robot) { } @Test - void unauthenticatedSearchShowsErrorAndLeavesTableEmpty(FxRobot robot) { + void unauthenticatedSearchShowsErrorAndLeavesTableEmpty() { // Fresh session, no login: the request cannot be authenticated. session.clear(); loadListPage(robot);