Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/test/java/ui/FxTestBase.java
Original file line number Diff line number Diff line change
@@ -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));
}
}
46 changes: 27 additions & 19 deletions src/test/java/ui/LoginFormControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,30 @@
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";

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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
26 changes: 17 additions & 9 deletions src/test/java/ui/MockFlowTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -68,7 +76,7 @@ void mockLoginPutsUserIntoSession() {

@Test
@SuppressWarnings("unchecked")
void detailsPageRendersSeededMockResource(FxRobot robot) {
void detailsPageRendersSeededMockResource() {
robot.interact(
() -> {
controller.setResourceId(SEEDED_RESOURCE_ID);
Expand All @@ -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());
Expand Down
33 changes: 21 additions & 12 deletions src/test/java/ui/ResourceDetailsControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -155,7 +164,7 @@ void cancelledDirectoryChooserDoesNotCallDownload(FxRobot robot) {
}

@Test
void backNavigatesToResourceList(FxRobot robot) {
void backNavigatesToResourceList() {
robot.clickOn("#backButton");

verify(navigator).goTo("/fxml/ResourceList.fxml");
Expand Down
Loading
Loading