From 366a54e8506a6b81113e2caded7fa6357e7c2f2c Mon Sep 17 00:00:00 2001 From: Timur Gauzhaev Date: Sat, 10 Oct 2026 12:32:10 +0300 Subject: [PATCH 1/5] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20css=20=D0=B8=20=D1=80=D0=B5=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B3=D0=BB=D0=B0=D0=B2?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/utils/Controller.java | 3 +++ .../services/impl/ResourceServiceImpl.java | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java index 2e25530..3386be1 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java @@ -14,4 +14,7 @@ public abstract class Controller implements SessionAware, NavigatorAware, Servic protected Session session; protected Navigator navigator; protected ServiceRegistry services; + + public void onLoad() { + } } diff --git a/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java b/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java index 9b566a7..e44b744 100644 --- a/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java +++ b/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java @@ -76,6 +76,21 @@ public List search( } } + @Override + public List search(ResourceFilter filter, long offset, long count, + String sortBy, String sortDir) { + ResourceFilterRequest req = new ResourceFilterRequest( + filter.getTitle(), filter.getDescription(), null, null); + try { + return apiClient.findResourcesByFilters(req, offset, count, sortBy, sortDir) + .stream().map(this::toEntity).toList(); + } catch (ApiException e) { + throw new ServiceException(e.getMessage(), e.getStatus()); + } catch (IOException | InterruptedException e) { + throw new ServiceException("Ошибка соединения с сервером"); + } + } + @Override public Resource update(UUID resourceId, String title, String description) { UpdateResourceRequest req = new UpdateResourceRequest(title, description); From ea5b06c0a7cf54424753c6f3daf6676bedc8694d Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Tue, 13 Oct 2026 14:33:22 +0300 Subject: [PATCH 2/5] chore: fix format and lint --- .../controllers/utils/Controller.java | 3 +-- .../services/impl/ResourceServiceImpl.java | 15 --------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java index 3386be1..40c46c7 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java @@ -15,6 +15,5 @@ public abstract class Controller implements SessionAware, NavigatorAware, Servic protected Navigator navigator; protected ServiceRegistry services; - public void onLoad() { - } + public void onLoad() {} } diff --git a/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java b/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java index e44b744..9b566a7 100644 --- a/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java +++ b/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java @@ -76,21 +76,6 @@ public List search( } } - @Override - public List search(ResourceFilter filter, long offset, long count, - String sortBy, String sortDir) { - ResourceFilterRequest req = new ResourceFilterRequest( - filter.getTitle(), filter.getDescription(), null, null); - try { - return apiClient.findResourcesByFilters(req, offset, count, sortBy, sortDir) - .stream().map(this::toEntity).toList(); - } catch (ApiException e) { - throw new ServiceException(e.getMessage(), e.getStatus()); - } catch (IOException | InterruptedException e) { - throw new ServiceException("Ошибка соединения с сервером"); - } - } - @Override public Resource update(UUID resourceId, String title, String description) { UpdateResourceRequest req = new UpdateResourceRequest(title, description); From 6cf4b7f42fed0c5f00c67655db8f4c4bda383cf8 Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Tue, 13 Oct 2026 14:38:34 +0300 Subject: [PATCH 3/5] chore: remove dead onLoad lifecycle hook --- .../controllers/utils/Controller.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java index 40c46c7..2e25530 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java @@ -14,6 +14,4 @@ public abstract class Controller implements SessionAware, NavigatorAware, Servic protected Session session; protected Navigator navigator; protected ServiceRegistry services; - - public void onLoad() {} } From f014330053da5559f4a3e33c00983659131626f7 Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Tue, 13 Oct 2026 15:11:46 +0300 Subject: [PATCH 4/5] test: add unit tests for ResourceFormController --- .../ui/ResourceFormControllerTest.java | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 src/test/java/com/DesktopApplicationClientJava/ui/ResourceFormControllerTest.java diff --git a/src/test/java/com/DesktopApplicationClientJava/ui/ResourceFormControllerTest.java b/src/test/java/com/DesktopApplicationClientJava/ui/ResourceFormControllerTest.java new file mode 100644 index 0000000..6599679 --- /dev/null +++ b/src/test/java/com/DesktopApplicationClientJava/ui/ResourceFormControllerTest.java @@ -0,0 +1,224 @@ +package com.DesktopApplicationClientJava.ui; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.DesktopApplicationClientJava.controllers.ResourceFormController; +import com.DesktopApplicationClientJava.entities.Resource; +import com.DesktopApplicationClientJava.navigation.Navigator; +import com.DesktopApplicationClientJava.services.ResourceService; +import com.DesktopApplicationClientJava.services.ServiceException; +import com.DesktopApplicationClientJava.services.ServiceRegistry; +import com.DesktopApplicationClientJava.session.Session; +import com.DesktopApplicationClientJava.utils.ControllerWiring; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +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; + +@ExtendWith(ApplicationExtension.class) +class ResourceFormControllerTest { + + private static final UUID RESOURCE_ID = UUID.randomUUID(); + private static final String TITLE = "Годовой отчёт"; + private static final String DESCRIPTION = "Описание документа"; + + private ResourceService resourceService; + private ServiceRegistry services; + private Navigator navigator; + private ResourceFormController controller; + private Stage stage; + + @Start + void start(Stage stage) throws Exception { + resourceService = mock(ResourceService.class); + services = mock(ServiceRegistry.class); + when(services.getResourceService()).thenReturn(resourceService); + navigator = mock(Navigator.class); + this.stage = stage; + loadForm(null); + } + + /** Loads the form as {@code initialize()} would see it (resourceId set before FXML load). */ + private void loadForm(UUID resourceId) throws Exception { + controller = new ResourceFormController(); + controller.setResourceId(resourceId); + FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/ResourceForm.fxml")); + loader.setControllerFactory( + clazz -> { + ControllerWiring.wire(controller, navigator, new Session(), services); + return controller; + }); + Parent root = loader.load(); + stage.setScene(new Scene(root)); + stage.show(); + } + + private void reloadForm(FxRobot robot, UUID resourceId) { + robot.interact( + () -> { + try { + loadForm(resourceId); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Test + void createModeShowsNewDocumentTitle(FxRobot robot) { + assertEquals("Новый документ", textOf(robot, "#formTitleLabel")); + assertFalse(robot.lookup("#errorLabel").queryAs(Label.class).isVisible()); + } + + @Test + void blankTitleShowsErrorAndDoesNotSave(FxRobot robot) { + robot.clickOn("#saveButton"); + + assertError(robot, "Название обязательно для заполнения"); + verify(resourceService, never()).create(anyString(), any()); + verify(resourceService, never()).update(any(), anyString(), any()); + verify(navigator, never()).goTo(anyString()); + } + + @Test + void shortTitleShowsErrorAndDoesNotSave(FxRobot robot) { + robot.clickOn("#titleField").write("ab"); + robot.clickOn("#saveButton"); + + assertError(robot, "Название не может быть короче 3 символов"); + verify(resourceService, never()).create(anyString(), any()); + verify(navigator, never()).goTo(anyString()); + } + + @Test + void longTitleShowsErrorAndDoesNotSave(FxRobot robot) { + robot.clickOn("#titleField").write("a".repeat(256)); + robot.clickOn("#saveButton"); + + assertError(robot, "Название не может быть длиннее 255 символов"); + verify(resourceService, never()).create(anyString(), any()); + verify(resourceService, never()).update(any(), anyString(), any()); + verify(navigator, never()).goTo(anyString()); + } + + @Test + void validInputCreatesAndNavigatesToList(FxRobot robot) { + robot.clickOn("#titleField").write(TITLE); + robot.clickOn("#descriptionArea").write(DESCRIPTION); + robot.clickOn("#saveButton"); + + verify(resourceService).create(TITLE, DESCRIPTION); + verify(navigator).goTo("/fxml/ResourceList.fxml"); + assertFalse(robot.lookup("#errorLabel").queryAs(Label.class).isVisible()); + } + + @Test + void blankDescriptionIsSavedAsNull(FxRobot robot) { + robot.clickOn("#titleField").write(TITLE); + robot.clickOn("#saveButton"); + + verify(resourceService).create(TITLE, null); + verify(navigator).goTo("/fxml/ResourceList.fxml"); + } + + @Test + void serviceErrorShowsInErrorLabelAndDoesNotNavigate(FxRobot robot) { + when(resourceService.create(anyString(), any())) + .thenThrow(new ServiceException("Не удалось сохранить")); + + robot.clickOn("#titleField").write(TITLE); + robot.clickOn("#saveButton"); + + assertError(robot, "Не удалось сохранить"); + verify(navigator, never()).goTo(anyString()); + } + + @Test + void cancelNavigatesToListWithoutSaving(FxRobot robot) { + robot.clickOn("#titleField").write(TITLE); + robot.clickOn("#cancelButton"); + + verify(navigator).goTo("/fxml/ResourceList.fxml"); + verify(resourceService, never()).create(anyString(), any()); + verify(resourceService, never()).update(any(), anyString(), any()); + } + + @Test + void editModePrefillsExistingValues(FxRobot robot) { + when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); + reloadForm(robot, RESOURCE_ID); + + assertEquals("Редактирование документа", textOf(robot, "#formTitleLabel")); + assertEquals("Старое название", fieldText(robot, "#titleField")); + assertEquals("Старое описание", areaText(robot, "#descriptionArea")); + } + + @Test + void editModeSaveUpdatesAndNavigatesToList(FxRobot robot) { + when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); + reloadForm(robot, RESOURCE_ID); + + robot.interact( + () -> robot.lookup("#titleField").queryAs(TextField.class).setText("Новое название")); + robot.clickOn("#saveButton"); + + verify(resourceService).update(RESOURCE_ID, "Новое название", "Старое описание"); + verify(resourceService, never()).create(anyString(), any()); + verify(navigator).goTo("/fxml/ResourceList.fxml"); + } + + @Test + void editModeLoadErrorShowsInErrorLabel(FxRobot robot) { + when(resourceService.getById(RESOURCE_ID)).thenThrow(new ServiceException("Ресурс не найден")); + reloadForm(robot, RESOURCE_ID); + + assertError(robot, "Ресурс не найден"); + } + + private Resource existingResource() { + return new Resource( + RESOURCE_ID, + "Старое название", + "Старое описание", + LocalDateTime.of(2024, 5, 6, 7, 8), + LocalDateTime.of(2024, 5, 7, 9, 10), + List.of()); + } + + private static void assertError(FxRobot robot, String expected) { + Label error = robot.lookup("#errorLabel").queryAs(Label.class); + assertTrue(error.isVisible()); + assertEquals(expected, error.getText()); + } + + private static String textOf(FxRobot robot, String query) { + return robot.lookup(query).queryAs(Label.class).getText(); + } + + private static String fieldText(FxRobot robot, String query) { + return robot.lookup(query).queryAs(TextField.class).getText(); + } + + private static String areaText(FxRobot robot, String query) { + return robot.lookup(query).queryAs(TextArea.class).getText(); + } +} From cc644e6a854b29d15d2cd5a40e28a9fcd65371d0 Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Tue, 13 Oct 2026 16:15:56 +0300 Subject: [PATCH 5/5] fix: react to late resourceId in details and form controllers --- .../ui/ResourceFormControllerTest.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/DesktopApplicationClientJava/ui/ResourceFormControllerTest.java b/src/test/java/com/DesktopApplicationClientJava/ui/ResourceFormControllerTest.java index 6599679..465aded 100644 --- a/src/test/java/com/DesktopApplicationClientJava/ui/ResourceFormControllerTest.java +++ b/src/test/java/com/DesktopApplicationClientJava/ui/ResourceFormControllerTest.java @@ -72,15 +72,13 @@ private void loadForm(UUID resourceId) throws Exception { stage.show(); } - private void reloadForm(FxRobot robot, UUID resourceId) { - robot.interact( - () -> { - try { - loadForm(resourceId); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + /** + * Production ordering (see Navigator.goTo): the form is loaded with no id, and the id is set + * afterwards via withController. Setting it here post-load must drive the controller into edit + * mode; pre-setting it before load would mask a controller that ignores late ids. + */ + private void openEditForm(FxRobot robot, UUID resourceId) { + robot.interact(() -> controller.setResourceId(resourceId)); } @Test @@ -165,7 +163,7 @@ void cancelNavigatesToListWithoutSaving(FxRobot robot) { @Test void editModePrefillsExistingValues(FxRobot robot) { when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); - reloadForm(robot, RESOURCE_ID); + openEditForm(robot, RESOURCE_ID); assertEquals("Редактирование документа", textOf(robot, "#formTitleLabel")); assertEquals("Старое название", fieldText(robot, "#titleField")); @@ -175,7 +173,7 @@ void editModePrefillsExistingValues(FxRobot robot) { @Test void editModeSaveUpdatesAndNavigatesToList(FxRobot robot) { when(resourceService.getById(RESOURCE_ID)).thenReturn(existingResource()); - reloadForm(robot, RESOURCE_ID); + openEditForm(robot, RESOURCE_ID); robot.interact( () -> robot.lookup("#titleField").queryAs(TextField.class).setText("Новое название")); @@ -189,7 +187,7 @@ void editModeSaveUpdatesAndNavigatesToList(FxRobot robot) { @Test void editModeLoadErrorShowsInErrorLabel(FxRobot robot) { when(resourceService.getById(RESOURCE_ID)).thenThrow(new ServiceException("Ресурс не найден")); - reloadForm(robot, RESOURCE_ID); + openEditForm(robot, RESOURCE_ID); assertError(robot, "Ресурс не найден"); }