From a652d0b2578108637154dd8d9114034f8631ac31 Mon Sep 17 00:00:00 2001 From: ZanDev32 <52073144+ZanDev32@users.noreply.github.com> Date: Mon, 7 Jul 2025 19:31:07 +0700 Subject: [PATCH 01/45] Update test unit --- pom.xml | 45 +++- src/main/java/module-info.java | 3 +- .../resources/com/starlight/view/mission.fxml | 2 +- .../java/com/starlight/api/ApiClientTest.java | 192 ++++++++++++++ .../com/starlight/api/UserApiServerTest.java | 244 ++++++++++++++++++ .../AuthorizationControllerTest.java | 160 ++++++++++++ .../controller/LoadingControllerTest.java | 118 +++++++++ .../java/com/starlight/models/PostTest.java | 215 +++++++++++++++ .../models/UserDataRepositoryTest.java | 198 ++++++++++++++ .../java/com/starlight/models/UserTest.java | 130 ++++++++++ .../com/starlight/util/FXMLLoadingTest.java | 21 +- .../starlight/util/FXMLVerificatorTest.java | 218 ++++++++++++++++ .../java/com/starlight/util/SessionTest.java | 163 ++++++++++++ 13 files changed, 1697 insertions(+), 12 deletions(-) create mode 100644 src/test/java/com/starlight/api/ApiClientTest.java create mode 100644 src/test/java/com/starlight/api/UserApiServerTest.java create mode 100644 src/test/java/com/starlight/controller/AuthorizationControllerTest.java create mode 100644 src/test/java/com/starlight/controller/LoadingControllerTest.java create mode 100644 src/test/java/com/starlight/models/PostTest.java create mode 100644 src/test/java/com/starlight/models/UserDataRepositoryTest.java create mode 100644 src/test/java/com/starlight/models/UserTest.java create mode 100644 src/test/java/com/starlight/util/FXMLVerificatorTest.java create mode 100644 src/test/java/com/starlight/util/SessionTest.java diff --git a/pom.xml b/pom.xml index e2405ee..a236b09 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ org.testfx openjfx-monocle - 11.0.2 + jdk-12.0.1+2 test @@ -131,6 +131,27 @@ 0.0.8 com.starlight.App + + --add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.glass.ui=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.glass.ui=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.javafx.util=ALL-UNNAMED + --add-exports=javafx.base/com.sun.javafx.logging=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.glass.ui=org.testfx + --add-opens=javafx.graphics/com.sun.glass.ui.monocle=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.javafx.scene.control=ALL-UNNAMED + --add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.glass.ui.monocle.input.devices=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.glass.ui.monocle=org.testfx + --add-exports=javafx.graphics/com.sun.glass.ui.monocle=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.glass.ui=ALL-UNNAMED,org.testfx.monocle + --add-opens=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.glass.ui.monocle.input=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.glass.ui.monocle.input.devices=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.javafx.application.HostServicesDelegate=ALL-UNNAMED + @@ -141,6 +162,28 @@ --add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.glass.ui=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.glass.ui=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.javafx.util=ALL-UNNAMED + --add-exports=javafx.base/com.sun.javafx.logging=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.glass.ui=org.testfx + --add-opens=javafx.graphics/com.sun.glass.ui.monocle=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.javafx.scene.control=ALL-UNNAMED + --add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.glass.ui.monocle.input.devices=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.glass.ui.monocle=org.testfx + --add-exports=javafx.graphics/com.sun.glass.ui.monocle=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.glass.ui=ALL-UNNAMED,org.testfx.monocle + --add-opens=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.glass.ui.monocle.input=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.glass.ui.monocle.input.devices=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.javafx.application.HostServicesDelegate=ALL-UNNAMED + --add-opens=com.starlight/com.starlight=ALL-UNNAMED + --add-opens=com.starlight/com.starlight.api=ALL-UNNAMED + --add-opens=com.starlight/com.starlight.models=ALL-UNNAMED + --add-opens=com.starlight/com.starlight.util=ALL-UNNAMED + --add-opens=com.starlight/com.starlight.controller=ALL-UNNAMED diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index d9bff58..688b7b6 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -12,9 +12,10 @@ requires jdk.httpserver; opens com.starlight to javafx.fxml; - opens com.starlight.controller to javafx.fxml; + opens com.starlight.controller; opens com.starlight.models; opens com.starlight.util; + opens com.starlight.api; exports com.starlight; exports com.starlight.controller; diff --git a/src/main/resources/com/starlight/view/mission.fxml b/src/main/resources/com/starlight/view/mission.fxml index 7d97f40..c874b88 100644 --- a/src/main/resources/com/starlight/view/mission.fxml +++ b/src/main/resources/com/starlight/view/mission.fxml @@ -10,7 +10,7 @@ - + diff --git a/src/test/java/com/starlight/api/ApiClientTest.java b/src/test/java/com/starlight/api/ApiClientTest.java new file mode 100644 index 0000000..3506b8c --- /dev/null +++ b/src/test/java/com/starlight/api/ApiClientTest.java @@ -0,0 +1,192 @@ +package com.starlight.api; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.starlight.api.ApiClient.ApiError; +import com.starlight.api.ApiClient.ApiException; +import com.starlight.models.User; + +/** + * Unit tests for {@link ApiClient}. + */ +public class ApiClientTest { + + private ApiClient apiClient; + + @BeforeEach + void setUp() { + apiClient = new ApiClient(); + } + + @Test + void testApiError_DefaultConstructor() { + ApiError error = new ApiError(); + assertEquals("Unknown error", error.getMessage()); + } + + @Test + void testApiError_WithMessage() { + String message = "Test error message"; + ApiError error = new ApiError(message); + assertEquals(message, error.getMessage()); + } + + @Test + void testApiError_SetMessage() { + ApiError error = new ApiError(); + String message = "New error message"; + error.setMessage(message); + assertEquals(message, error.getMessage()); + } + + @Test + void testApiError_NullMessage() { + ApiError error = new ApiError(); + error.setMessage(null); + assertEquals("Unknown error", error.getMessage()); + } + + @Test + void testApiException_Constructor() { + int statusCode = 400; + String message = "Bad Request"; + ApiException exception = new ApiException(statusCode, message); + + assertEquals(statusCode, exception.getStatusCode()); + assertEquals(message, exception.getMessage()); + } + + @Test + void testApiException_InheritanceFromException() { + ApiException exception = new ApiException(500, "Internal Server Error"); + assertTrue(exception instanceof Exception); + } + + // Note: Testing the actual HTTP operations would require either: + // 1. A mock HTTP server for integration tests + // 2. More sophisticated mocking of URL/HttpURLConnection + // 3. Refactoring ApiClient to use dependency injection for HTTP client + + // For now, we'll test the basic structure and exception handling + @Test + void testLoginMethodExists() { + // Verify that the method exists and can be called + // This will throw an exception due to network connectivity, but that's expected + assertThrows(ApiException.class, () -> { + apiClient.login("test@example.com", "password"); + }); + } + + @Test + void testRegisterMethodExists() { + // Verify that the method exists and can be called + // This will throw an exception due to network connectivity, but that's expected + assertThrows(ApiException.class, () -> { + apiClient.register("testuser", "test@example.com", "password"); + }); + } + + @Test + void testApiClientConstructor() { + // Test that ApiClient can be instantiated without issues + assertDoesNotThrow(() -> { + ApiClient client = new ApiClient(); + assertNotNull(client); + }); + } + + // Test XML handling preparation + @Test + void testUserObjectForSerialization() { + // Test that User objects can be prepared for XML serialization + User user = new User(); + user.username = "testuser"; + user.email = "test@example.com"; + user.password = "password"; + + // This tests that the object creation works without serialization errors + assertDoesNotThrow(() -> { + assertNotNull(user.username); + assertNotNull(user.email); + assertNotNull(user.password); + }); + } + + @Test + void testApiExceptionWithVariousStatusCodes() { + int[] statusCodes = {400, 401, 403, 404, 409, 500, 502, 503}; + String[] messages = { + "Bad Request", "Unauthorized", "Forbidden", "Not Found", + "Conflict", "Internal Server Error", "Bad Gateway", "Service Unavailable" + }; + + for (int i = 0; i < statusCodes.length; i++) { + ApiException exception = new ApiException(statusCodes[i], messages[i]); + assertEquals(statusCodes[i], exception.getStatusCode()); + assertEquals(messages[i], exception.getMessage()); + } + } + + @Test + void testApiErrorEdgeCases() { + ApiError error = new ApiError(""); + assertEquals("", error.getMessage()); + + error = new ApiError(" "); + assertEquals(" ", error.getMessage()); + + error = new ApiError("Multi\nline\nerror"); + assertEquals("Multi\nline\nerror", error.getMessage()); + } + + // Test that the static inner classes can be instantiated independently + @Test + void testStaticInnerClassInstantiation() { + assertDoesNotThrow(() -> { + ApiError error = new ApiError("Test"); + assertNotNull(error); + + ApiException exception = new ApiException(200, "Success"); + assertNotNull(exception); + }); + } + + @Test + void testLoginWithNullParameters() { + // Test behavior with null parameters + assertThrows(ApiException.class, () -> { + apiClient.login(null, "password"); + }); + + assertThrows(ApiException.class, () -> { + apiClient.login("test@example.com", null); + }); + } + + @Test + void testRegisterWithNullParameters() { + // Test behavior with null parameters + assertThrows(ApiException.class, () -> { + apiClient.register(null, "test@example.com", "password"); + }); + + assertThrows(ApiException.class, () -> { + apiClient.register("testuser", null, "password"); + }); + + assertThrows(ApiException.class, () -> { + apiClient.register("testuser", "test@example.com", null); + }); + } + + @Test + void testApiExceptionStatusCodeZero() { + // Test the case where network error results in status code 0 + ApiException exception = new ApiException(0, "Network error: Connection refused"); + assertEquals(0, exception.getStatusCode()); + assertTrue(exception.getMessage().contains("Network error")); + } +} diff --git a/src/test/java/com/starlight/api/UserApiServerTest.java b/src/test/java/com/starlight/api/UserApiServerTest.java new file mode 100644 index 0000000..7acdc2d --- /dev/null +++ b/src/test/java/com/starlight/api/UserApiServerTest.java @@ -0,0 +1,244 @@ +package com.starlight.api; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link UserApiServer}. + */ +public class UserApiServerTest { + + private UserApiServer server; + private static final int TEST_PORT = 9000; // Use a different port range to avoid conflicts + + @BeforeEach + void setUp() throws IOException { + // Create server on a test port with some randomization to avoid conflicts + int basePort = TEST_PORT + (int)(Math.random() * 1000); + server = new UserApiServer(basePort); + } + + @AfterEach + void tearDown() { + if (server != null) { + server.stop(); + } + } + + @Test + void testServerConstruction() throws IOException { + // Test that server can be constructed without throwing + assertDoesNotThrow(() -> { + int testPort = 9100 + (int)(Math.random() * 500); + UserApiServer testServer = new UserApiServer(testPort); + assertNotNull(testServer); + testServer.stop(); // Clean up + }); + } + + @Test + void testServerConstructionWithPortInUse() throws IOException { + // Start first server + int basePort = 9200 + (int)(Math.random() * 100); + UserApiServer firstServer = new UserApiServer(basePort); + firstServer.start(); + + try { + // Creating second server on same port should work (it will find next available port) + assertDoesNotThrow(() -> { + UserApiServer secondServer = new UserApiServer(basePort); + assertNotNull(secondServer); + secondServer.stop(); + }); + } finally { + firstServer.stop(); + } + } + + @Test + void testStartServer() { + assertDoesNotThrow(() -> { + server.start(); + }); + } + + @Test + void testStopServer() { + server.start(); + assertDoesNotThrow(() -> { + server.stop(); + }); + } + + @Test + void testStartStopCycle() { + // Test start/stop cycle (Note: HttpServer cannot be restarted once stopped) + assertDoesNotThrow(() -> { + server.start(); + Thread.sleep(10); // Give server time to start + server.stop(); + Thread.sleep(10); // Give server time to stop + // Cannot restart the same HttpServer instance - this is a Java HttpServer limitation + }); + } + + @Test + void testDoubleStart() { + // Starting an already started server should not cause issues + assertDoesNotThrow(() -> { + server.start(); + server.start(); // Second start should be ignored + }); + } + + @Test + void testDoubleStop() { + // Stopping an already stopped server should not cause issues + server.start(); + assertDoesNotThrow(() -> { + server.stop(); + server.stop(); // Second stop should be ignored + }); + } + + @Test + void testStopWithoutStart() { + // Stopping a server that was never started should not cause issues + assertDoesNotThrow(() -> { + server.stop(); + }); + } + + @Test + void testServerState() { + // Test that server can handle state changes properly + assertDoesNotThrow(() -> { + // Initial state - not started + server.stop(); // Should not throw + + // Start server + server.start(); + Thread.sleep(10); // Give server time to start + + // Stop server + server.stop(); + Thread.sleep(10); // Give server time to stop + + // Note: HttpServer cannot be restarted once stopped - this is a Java limitation + // So we don't test restart here + }); + } + + @Test + void testRestartWithNewInstance() throws IOException { + // Test restart functionality by creating new server instances + // (since HttpServer cannot be restarted once stopped) + int testPort = 9800 + (int)(Math.random() * 100); + + assertDoesNotThrow(() -> { + // First instance + UserApiServer server1 = new UserApiServer(testPort); + server1.start(); + server1.stop(); + + // Second instance on same port (should work after first is stopped) + UserApiServer server2 = new UserApiServer(testPort); + server2.start(); + server2.stop(); + }); + } + + @Test + void testMultipleServerInstances() throws IOException { + // Test that multiple server instances can coexist (on different ports) + int port1 = 9300 + (int)(Math.random() * 50); + int port2 = 9350 + (int)(Math.random() * 50); + UserApiServer server1 = new UserApiServer(port1); + UserApiServer server2 = new UserApiServer(port2); + + try { + assertDoesNotThrow(() -> { + server1.start(); + server2.start(); + }); + + assertDoesNotThrow(() -> { + server1.stop(); + server2.stop(); + }); + } finally { + server1.stop(); + server2.stop(); + } + } + + @Test + void testServerConstructionPortRange() { + // Test that server handles port binding failures gracefully + // by trying multiple ports (this tests the port binding retry logic) + assertDoesNotThrow(() -> { + // This should work even if some ports are in use + int testPort = 9400 + (int)(Math.random() * 100); + UserApiServer testServer = new UserApiServer(testPort); + testServer.stop(); + }); + } + + @Test + void testServerResourceCleanup() throws IOException { + // Test that server properly cleans up resources + int testPort = 9500 + (int)(Math.random() * 100); + UserApiServer testServer = new UserApiServer(testPort); + + testServer.start(); + testServer.stop(); + + // Should be able to create another server on same port after proper cleanup + assertDoesNotThrow(() -> { + UserApiServer secondServer = new UserApiServer(testPort); + secondServer.stop(); + }); + } + + @Test + void testServerStartAfterException() throws IOException { + // Test that server can be started after handling construction exceptions + try { + // This should succeed since we handle port conflicts + int testPort = 9600 + (int)(Math.random() * 100); + UserApiServer testServer = new UserApiServer(testPort); + testServer.start(); + testServer.stop(); + } catch (Exception e) { + fail("Server construction should handle port conflicts gracefully"); + } + } + + // Note: Testing the actual HTTP endpoints would require integration tests + // with actual HTTP requests. For unit tests, we focus on the lifecycle + // and basic functionality of the server. + + @Test + void testServerLifecycle() { + // Test complete server lifecycle + assertDoesNotThrow(() -> { + // Construction phase + int testPort = 9700 + (int)(Math.random() * 100); + UserApiServer lifecycleServer = new UserApiServer(testPort); + + // Startup phase + lifecycleServer.start(); + + // Runtime phase (server is running) + // In a real scenario, this is where HTTP requests would be handled + + // Shutdown phase + lifecycleServer.stop(); + }); + } +} diff --git a/src/test/java/com/starlight/controller/AuthorizationControllerTest.java b/src/test/java/com/starlight/controller/AuthorizationControllerTest.java new file mode 100644 index 0000000..2becb96 --- /dev/null +++ b/src/test/java/com/starlight/controller/AuthorizationControllerTest.java @@ -0,0 +1,160 @@ +package com.starlight.controller; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testfx.framework.junit5.ApplicationTest; + +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; + +/** + * Unit tests for {@link AuthorizationController}. + */ +public class AuthorizationControllerTest extends ApplicationTest { + + private AuthorizationController controller; + private BorderPane testPane; + + @BeforeAll + static void setUpClass() { + // Ensure JavaFX is initialized for headless testing + System.setProperty("java.awt.headless", "true"); + System.setProperty("testfx.robot", "glass"); + System.setProperty("testfx.headless", "true"); + System.setProperty("prism.order", "sw"); + System.setProperty("prism.text", "t2k"); + System.setProperty("glass.platform", "Monocle"); + System.setProperty("monocle.platform", "Headless"); + } + + @Override + public void start(Stage stage) throws Exception { + // Create a minimal test scene + testPane = new BorderPane(); + Scene scene = new Scene(testPane, 800, 600); + stage.setScene(scene); + + // Create the controller + controller = new AuthorizationController(); + + stage.show(); + } + + @BeforeEach + void setUp() { + // Reset the test pane for each test + Platform.runLater(() -> { + testPane.setRight(null); + testPane.setLeft(null); + testPane.setTop(null); + testPane.setBottom(null); + testPane.setCenter(null); + }); + } + + @Test + void testControllerInstantiation() { + assertNotNull(controller); + } + + @Test + void testShowLoginViewMethodExists() { + // Test that the method exists and can be called + // Note: This will fail due to FXML resource loading in actual implementation + // but tests that the method signature is correct + assertDoesNotThrow(() -> { + try { + controller.showLoginView(); + } catch (NullPointerException e) { + // Expected in unit test environment due to missing FXML resources + assertTrue(e.getMessage() == null || e.getMessage().isEmpty() || + e.getMessage().contains("null")); + } + }); + } + + @Test + void testShowRegisterViewMethodExists() { + // Test that the method exists and can be called + // Note: This will fail due to FXML resource loading in actual implementation + // but tests that the method signature is correct + assertDoesNotThrow(() -> { + try { + controller.showRegisterView(); + } catch (NullPointerException e) { + // Expected in unit test environment due to missing FXML resources + assertTrue(e.getMessage() == null || e.getMessage().isEmpty() || + e.getMessage().contains("null")); + } + }); + } + + @Test + void testControllerImplementsInitializable() { + // Test that AuthorizationController implements Initializable + assertTrue(controller instanceof javafx.fxml.Initializable); + } + + @Test + void testInitializeMethodExists() { + // Test that the initialize method exists using reflection + assertDoesNotThrow(() -> { + var initializeMethod = AuthorizationController.class.getMethod("initialize", + java.net.URL.class, java.util.ResourceBundle.class); + assertNotNull(initializeMethod); + }); + } + + @Test + void testMethodsExist() { + // Test that the required methods exist using reflection + assertDoesNotThrow(() -> { + var showLoginMethod = AuthorizationController.class.getMethod("showLoginView"); + assertNotNull(showLoginMethod); + + var showRegisterMethod = AuthorizationController.class.getMethod("showRegisterView"); + assertNotNull(showRegisterMethod); + }); + } + + @Test + void testControllerCanBeCreated() { + // Test that multiple controller instances can be created + assertDoesNotThrow(() -> { + AuthorizationController controller1 = new AuthorizationController(); + AuthorizationController controller2 = new AuthorizationController(); + + assertNotNull(controller1); + assertNotNull(controller2); + assertNotSame(controller1, controller2); + }); + } + + // Note: Full testing of this controller would require: + // 1. Mock FXML resources + // 2. Mock LoginViewController and RegisterViewController + // 3. Integration test environment with actual FXML loading + // + // For unit testing purposes, we focus on testing the controller structure + // and ensuring it can be instantiated without errors. + + @Test + void testInitializeWithNullParameters() { + // Test initialize method with null parameters + assertDoesNotThrow(() -> { + try { + controller.initialize(null, null); + } catch (Exception e) { + // Expected to fail due to resource loading, but method should exist + assertTrue(e instanceof NullPointerException || + e instanceof java.io.IOException || + e.getCause() instanceof java.io.IOException); + } + }); + } +} diff --git a/src/test/java/com/starlight/controller/LoadingControllerTest.java b/src/test/java/com/starlight/controller/LoadingControllerTest.java new file mode 100644 index 0000000..db2d062 --- /dev/null +++ b/src/test/java/com/starlight/controller/LoadingControllerTest.java @@ -0,0 +1,118 @@ +package com.starlight.controller; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testfx.framework.junit5.ApplicationTest; + +import javafx.scene.Scene; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +/** + * Unit tests for {@link LoadingController}. + */ +public class LoadingControllerTest extends ApplicationTest { + + private LoadingController controller; + + @BeforeAll + static void setUpClass() { + // Ensure JavaFX is initialized for headless testing + System.setProperty("java.awt.headless", "true"); + System.setProperty("testfx.robot", "glass"); + System.setProperty("testfx.headless", "true"); + System.setProperty("prism.order", "sw"); + System.setProperty("prism.text", "t2k"); + System.setProperty("glass.platform", "Monocle"); + System.setProperty("monocle.platform", "Headless"); + } + + @Override + public void start(Stage stage) throws Exception { + // Create a minimal test scene + VBox root = new VBox(); + Scene scene = new Scene(root, 400, 300); + stage.setScene(scene); + + // Create the controller + controller = new LoadingController(); + + stage.show(); + } + + @Test + void testControllerInstantiation() { + assertNotNull(controller); + } + + @Test + void testControllerCanBeCreated() { + // Test that multiple controller instances can be created + assertDoesNotThrow(() -> { + LoadingController controller1 = new LoadingController(); + LoadingController controller2 = new LoadingController(); + + assertNotNull(controller1); + assertNotNull(controller2); + assertNotSame(controller1, controller2); + }); + } + + @Test + void testInitializeMethodExists() { + // Test that the initialize method exists using reflection + assertDoesNotThrow(() -> { + var initializeMethod = LoadingController.class.getDeclaredMethod("initialize"); + assertNotNull(initializeMethod); + + // Check that it's annotated with @FXML + assertTrue(initializeMethod.isAnnotationPresent(javafx.fxml.FXML.class)); + }); + } + + @Test + void testInitializeMethod() { + // Test that the initialize method can be called + assertDoesNotThrow(() -> { + // Use reflection to call the private initialize method + var initializeMethod = LoadingController.class.getDeclaredMethod("initialize"); + initializeMethod.setAccessible(true); + initializeMethod.invoke(controller); + }); + } + + @Test + void testControllerStructure() { + // Test that the controller has the expected structure + Class controllerClass = LoadingController.class; + + // Should be in the correct package + assertEquals("com.starlight.controller", controllerClass.getPackage().getName()); + + // Should be a public class + assertTrue(java.lang.reflect.Modifier.isPublic(controllerClass.getModifiers())); + } + + @Test + void testMultipleInitializeCalls() { + // Test that initialize can be called multiple times without issues + assertDoesNotThrow(() -> { + var initializeMethod = LoadingController.class.getDeclaredMethod("initialize"); + initializeMethod.setAccessible(true); + + // Call initialize multiple times + initializeMethod.invoke(controller); + initializeMethod.invoke(controller); + initializeMethod.invoke(controller); + }); + } + + @Test + void testControllerHasNoPublicFields() { + // Ensure the controller doesn't expose any public fields (good encapsulation) + var fields = LoadingController.class.getFields(); + assertEquals(0, fields.length, "Controller should not have public fields"); + } +} diff --git a/src/test/java/com/starlight/models/PostTest.java b/src/test/java/com/starlight/models/PostTest.java new file mode 100644 index 0000000..10fa650 --- /dev/null +++ b/src/test/java/com/starlight/models/PostTest.java @@ -0,0 +1,215 @@ +package com.starlight.models; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link Post} data model. + */ +public class PostTest { + private Post post; + + @BeforeEach + void setUp() { + post = new Post(); + } + + @Test + void testPostCreation() { + assertNotNull(post); + assertNull(post.uuid); + assertNull(post.username); + assertNull(post.profilepicture); + assertNull(post.title); + assertNull(post.description); + assertNull(post.ingredients); + assertNull(post.directions); + assertNull(post.image); + assertNull(post.rating); + assertNull(post.uploadtime); + assertNull(post.likecount); + } + + @Test + void testSetAndGetUuid() { + String testUuid = "123e4567-e89b-12d3-a456-426614174000"; + post.uuid = testUuid; + assertEquals(testUuid, post.uuid); + } + + @Test + void testSetAndGetUsername() { + String testUsername = "chef123"; + post.username = testUsername; + assertEquals(testUsername, post.username); + } + + @Test + void testSetAndGetProfilePicture() { + String testProfilePicture = "/images/profiles/chef123.jpg"; + post.profilepicture = testProfilePicture; + assertEquals(testProfilePicture, post.profilepicture); + } + + @Test + void testSetAndGetTitle() { + String testTitle = "Delicious Chocolate Cake"; + post.title = testTitle; + assertEquals(testTitle, post.title); + } + + @Test + void testSetAndGetDescription() { + String testDescription = "A moist and rich chocolate cake perfect for any occasion."; + post.description = testDescription; + assertEquals(testDescription, post.description); + } + + @Test + void testSetAndGetIngredients() { + String testIngredients = "2 cups flour, 1 cup sugar, 3 eggs, 1/2 cup cocoa powder"; + post.ingredients = testIngredients; + assertEquals(testIngredients, post.ingredients); + } + + @Test + void testSetAndGetDirections() { + String testDirections = "1. Mix dry ingredients. 2. Add wet ingredients. 3. Bake at 350Β°F for 30 minutes."; + post.directions = testDirections; + assertEquals(testDirections, post.directions); + } + + @Test + void testSetAndGetImage() { + String testImage = "/images/posts/chocolate_cake.jpg"; + post.image = testImage; + assertEquals(testImage, post.image); + } + + @Test + void testSetAndGetRating() { + String testRating = "4.5"; + post.rating = testRating; + assertEquals(testRating, post.rating); + } + + @Test + void testSetAndGetUploadTime() { + String testUploadTime = "2025-07-07T10:30:00Z"; + post.uploadtime = testUploadTime; + assertEquals(testUploadTime, post.uploadtime); + } + + @Test + void testSetAndGetLikeCount() { + String testLikeCount = "42"; + post.likecount = testLikeCount; + assertEquals(testLikeCount, post.likecount); + } + + @Test + void testPostWithAllFieldsSet() { + post.uuid = "550e8400-e29b-41d4-a716-446655440000"; + post.username = "masterchef"; + post.profilepicture = "/images/profiles/masterchef.png"; + post.title = "Perfect Pasta Carbonara"; + post.description = "Traditional Italian carbonara with eggs, cheese, and pancetta."; + post.ingredients = "400g spaghetti, 200g pancetta, 4 eggs, 100g Pecorino Romano, black pepper"; + post.directions = "1. Cook pasta. 2. Fry pancetta. 3. Mix eggs and cheese. 4. Combine all together off heat."; + post.image = "/images/posts/carbonara.jpg"; + post.rating = "4.8"; + post.uploadtime = "2025-07-07T15:45:30Z"; + post.likecount = "127"; + + assertEquals("550e8400-e29b-41d4-a716-446655440000", post.uuid); + assertEquals("masterchef", post.username); + assertEquals("/images/profiles/masterchef.png", post.profilepicture); + assertEquals("Perfect Pasta Carbonara", post.title); + assertEquals("Traditional Italian carbonara with eggs, cheese, and pancetta.", post.description); + assertEquals("400g spaghetti, 200g pancetta, 4 eggs, 100g Pecorino Romano, black pepper", post.ingredients); + assertEquals("1. Cook pasta. 2. Fry pancetta. 3. Mix eggs and cheese. 4. Combine all together off heat.", post.directions); + assertEquals("/images/posts/carbonara.jpg", post.image); + assertEquals("4.8", post.rating); + assertEquals("2025-07-07T15:45:30Z", post.uploadtime); + assertEquals("127", post.likecount); + } + + @Test + void testPostFieldsCanBeNull() { + // Set some values first + post.uuid = "test-uuid"; + post.username = "testuser"; + post.title = "Test Title"; + + // Then set to null + post.uuid = null; + post.username = null; + post.profilepicture = null; + post.title = null; + post.description = null; + post.ingredients = null; + post.directions = null; + post.image = null; + post.rating = null; + post.uploadtime = null; + post.likecount = null; + + assertNull(post.uuid); + assertNull(post.username); + assertNull(post.profilepicture); + assertNull(post.title); + assertNull(post.description); + assertNull(post.ingredients); + assertNull(post.directions); + assertNull(post.image); + assertNull(post.rating); + assertNull(post.uploadtime); + assertNull(post.likecount); + } + + @Test + void testPostFieldsCanBeEmpty() { + post.uuid = ""; + post.username = ""; + post.profilepicture = ""; + post.title = ""; + post.description = ""; + post.ingredients = ""; + post.directions = ""; + post.image = ""; + post.rating = ""; + post.uploadtime = ""; + post.likecount = ""; + + assertEquals("", post.uuid); + assertEquals("", post.username); + assertEquals("", post.profilepicture); + assertEquals("", post.title); + assertEquals("", post.description); + assertEquals("", post.ingredients); + assertEquals("", post.directions); + assertEquals("", post.image); + assertEquals("", post.rating); + assertEquals("", post.uploadtime); + assertEquals("", post.likecount); + } + + @Test + void testPostWithSpecialCharacters() { + post.title = "CafΓ© au Lait & Croissants"; + post.description = "A French breakfast with cafΓ© au lait β˜• and buttery croissants πŸ₯"; + post.ingredients = "CafΓ© beans (organic), milk, butter, flour, eggs"; + post.directions = "1. Brew coffee β˜• 2. Heat milk πŸ₯› 3. Bake croissants @ 200Β°C"; + post.rating = "4.9"; + post.likecount = "1,234"; + + assertEquals("CafΓ© au Lait & Croissants", post.title); + assertEquals("A French breakfast with cafΓ© au lait β˜• and buttery croissants πŸ₯", post.description); + assertEquals("CafΓ© beans (organic), milk, butter, flour, eggs", post.ingredients); + assertEquals("1. Brew coffee β˜• 2. Heat milk πŸ₯› 3. Bake croissants @ 200Β°C", post.directions); + assertEquals("4.9", post.rating); + assertEquals("1,234", post.likecount); + } +} diff --git a/src/test/java/com/starlight/models/UserDataRepositoryTest.java b/src/test/java/com/starlight/models/UserDataRepositoryTest.java new file mode 100644 index 0000000..218ac50 --- /dev/null +++ b/src/test/java/com/starlight/models/UserDataRepositoryTest.java @@ -0,0 +1,198 @@ +package com.starlight.models; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link UserDataRepository}. + */ +public class UserDataRepositoryTest { + private Path tempFile; + private Path tempDummyFile; + private UserDataRepository repository; + + @BeforeEach + void setUp() throws IOException { + tempFile = Files.createTempFile("users", ".xml"); + tempDummyFile = Files.createTempFile("usersDummy", ".xml"); + repository = new UserDataRepository(tempFile.toString()); + + // Create some dummy data for testing + createDummyData(); + } + + @AfterEach + void tearDown() throws IOException { + Files.deleteIfExists(tempFile); + Files.deleteIfExists(tempDummyFile); + } + + private void createDummyData() throws IOException { + String dummyXml = "\n" + + " \n" + + " dummy1\n" + + " dummy1@example.com\n" + + " Dummy User 1\n" + + " password1\n" + + " 1990-01-01\n" + + " \n" + + " \n" + + " dummy2\n" + + " dummy2@example.com\n" + + " Dummy User 2\n" + + " password2\n" + + " 1985-05-15\n" + + " \n" + + ""; + Files.writeString(tempDummyFile, dummyXml); + } + + @Test + void testSaveAndLoadUsers() { + List users = new ArrayList<>(); + + User user1 = new User(); + user1.username = "testuser1"; + user1.email = "test1@example.com"; + user1.fullname = "Test User 1"; + user1.password = "password123"; + user1.birthDay = "1995-03-10"; + users.add(user1); + + User user2 = new User(); + user2.username = "testuser2"; + user2.email = "test2@example.com"; + user2.fullname = "Test User 2"; + user2.password = "password456"; + user2.birthDay = "1992-07-20"; + users.add(user2); + + repository.saveUsers(users); + assertTrue(Files.exists(tempFile) && tempFile.toFile().length() > 0); + + List loaded = repository.loadUsers(false); // Don't include dummy data + assertEquals(2, loaded.size()); + + User loadedUser1 = loaded.get(0); + assertEquals(user1.username, loadedUser1.username); + assertEquals(user1.email, loadedUser1.email); + assertEquals(user1.fullname, loadedUser1.fullname); + assertEquals(user1.password, loadedUser1.password); + assertEquals(user1.birthDay, loadedUser1.birthDay); + + User loadedUser2 = loaded.get(1); + assertEquals(user2.username, loadedUser2.username); + assertEquals(user2.email, loadedUser2.email); + assertEquals(user2.fullname, loadedUser2.fullname); + assertEquals(user2.password, loadedUser2.password); + assertEquals(user2.birthDay, loadedUser2.birthDay); + } + + @Test + void testLoadUsersWithEmptyFile() { + // Ensure the file is empty + try { + Files.writeString(tempFile, ""); + } catch (IOException e) { + fail("Failed to create empty file: " + e.getMessage()); + } + + List users = repository.loadUsers(false); + assertNotNull(users); + // When the main file is empty, the repository falls back to dummy data + // even when includeDummy is false (this is the intended behavior) + assertEquals(4, users.size()); // Should load dummy users as fallback + } + + @Test + void testLoadUsersIncludingDummy() { + // Since we're using a custom path, this won't actually load dummy data + // but we can test the method call + List users = repository.loadUsers(true); + assertNotNull(users); + } + + @Test + void testDeleteUser() { + // First, save some users + List users = new ArrayList<>(); + + User user1 = new User(); + user1.username = "userToDelete"; + user1.email = "delete@example.com"; + user1.fullname = "Delete Me"; + user1.password = "password"; + user1.birthDay = "1990-01-01"; + users.add(user1); + + User user2 = new User(); + user2.username = "userToKeep"; + user2.email = "keep@example.com"; + user2.fullname = "Keep Me"; + user2.password = "password"; + user2.birthDay = "1990-01-01"; + users.add(user2); + + repository.saveUsers(users); + + // Delete one user + boolean deleted = repository.deleteUser("userToDelete"); + assertTrue(deleted); + + // Verify only one user remains + List remaining = repository.loadUsers(false); + assertEquals(1, remaining.size()); + assertEquals("userToKeep", remaining.get(0).username); + } + + @Test + void testDeleteNonExistentUser() { + // Save one user + List users = new ArrayList<>(); + User user = new User(); + user.username = "existingUser"; + user.email = "existing@example.com"; + users.add(user); + repository.saveUsers(users); + + // Try to delete non-existent user + boolean deleted = repository.deleteUser("nonExistentUser"); + assertFalse(deleted); + + // Verify original user still exists + List remaining = repository.loadUsers(false); + assertEquals(1, remaining.size()); + assertEquals("existingUser", remaining.get(0).username); + } + + @Test + void testSaveUsersThrowsExceptionOnInvalidPath() { + UserDataRepository invalidRepo = new UserDataRepository("/invalid/path/that/does/not/exist.xml"); + List users = new ArrayList<>(); + User user = new User(); + user.username = "test"; + users.add(user); + + assertThrows(RuntimeException.class, () -> invalidRepo.saveUsers(users)); + } + + @Test + void testDefaultConstructor() { + // Test that we can create a repository with default constructor + UserDataRepository defaultRepo = new UserDataRepository(); + assertNotNull(defaultRepo); + + // Should be able to call methods without throwing exceptions + List users = defaultRepo.loadUsers(false); + assertNotNull(users); + } +} diff --git a/src/test/java/com/starlight/models/UserTest.java b/src/test/java/com/starlight/models/UserTest.java new file mode 100644 index 0000000..f4f03ef --- /dev/null +++ b/src/test/java/com/starlight/models/UserTest.java @@ -0,0 +1,130 @@ +package com.starlight.models; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link User} data model. + */ +public class UserTest { + private User user; + + @BeforeEach + void setUp() { + user = new User(); + } + + @Test + void testUserCreation() { + assertNotNull(user); + assertNull(user.username); + assertNull(user.email); + assertNull(user.fullname); + assertNull(user.password); + assertNull(user.birthDay); + } + + @Test + void testSetAndGetUsername() { + String testUsername = "testuser123"; + user.username = testUsername; + assertEquals(testUsername, user.username); + } + + @Test + void testSetAndGetEmail() { + String testEmail = "test@example.com"; + user.email = testEmail; + assertEquals(testEmail, user.email); + } + + @Test + void testSetAndGetFullname() { + String testFullname = "John Doe"; + user.fullname = testFullname; + assertEquals(testFullname, user.fullname); + } + + @Test + void testSetAndGetPassword() { + String testPassword = "securePassword123"; + user.password = testPassword; + assertEquals(testPassword, user.password); + } + + @Test + void testSetAndGetBirthDay() { + String testBirthDay = "1990-05-15"; + user.birthDay = testBirthDay; + assertEquals(testBirthDay, user.birthDay); + } + + @Test + void testUserWithAllFieldsSet() { + user.username = "johndoe"; + user.email = "john.doe@example.com"; + user.fullname = "John Doe"; + user.password = "myPassword123"; + user.birthDay = "1985-12-25"; + + assertEquals("johndoe", user.username); + assertEquals("john.doe@example.com", user.email); + assertEquals("John Doe", user.fullname); + assertEquals("myPassword123", user.password); + assertEquals("1985-12-25", user.birthDay); + } + + @Test + void testUserFieldsCanBeNull() { + // Ensure that setting fields to null works + user.username = "test"; + user.email = "test@example.com"; + user.fullname = "Test User"; + user.password = "password"; + user.birthDay = "1990-01-01"; + + user.username = null; + user.email = null; + user.fullname = null; + user.password = null; + user.birthDay = null; + + assertNull(user.username); + assertNull(user.email); + assertNull(user.fullname); + assertNull(user.password); + assertNull(user.birthDay); + } + + @Test + void testUserFieldsCanBeEmpty() { + user.username = ""; + user.email = ""; + user.fullname = ""; + user.password = ""; + user.birthDay = ""; + + assertEquals("", user.username); + assertEquals("", user.email); + assertEquals("", user.fullname); + assertEquals("", user.password); + assertEquals("", user.birthDay); + } + + @Test + void testUserWithSpecialCharacters() { + user.username = "user_with-special.chars"; + user.email = "test+tag@example-domain.co.uk"; + user.fullname = "JosΓ© MarΓ­a O'Connor"; + user.password = "P@ssw0rd!#$"; + user.birthDay = "2000-02-29"; // Leap year + + assertEquals("user_with-special.chars", user.username); + assertEquals("test+tag@example-domain.co.uk", user.email); + assertEquals("JosΓ© MarΓ­a O'Connor", user.fullname); + assertEquals("P@ssw0rd!#$", user.password); + assertEquals("2000-02-29", user.birthDay); + } +} diff --git a/src/test/java/com/starlight/util/FXMLLoadingTest.java b/src/test/java/com/starlight/util/FXMLLoadingTest.java index 379bd57..0946be9 100644 --- a/src/test/java/com/starlight/util/FXMLLoadingTest.java +++ b/src/test/java/com/starlight/util/FXMLLoadingTest.java @@ -10,7 +10,6 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Parent; -import javafx.stage.Stage; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -23,10 +22,20 @@ public class FXMLLoadingTest extends ApplicationTest { static { - // Headless mode for TestFX (helpful for CI and local dev) + // Enable TestFX headless mode so JavaFX doesn't require a display System.setProperty("testfx.robot", "glass"); + System.setProperty("testfx.headless", "true"); + System.setProperty("glass.platform", "Monocle"); + System.setProperty("monocle.platform", "Headless"); + System.setProperty("prism.order", "sw"); + System.setProperty("prism.text", "t2k"); System.setProperty("java.awt.headless", "true"); - System.setProperty("headless", "true"); + + // Required for proper module access with Monocle in JavaFX 19 + System.setProperty("javafx.platform", "monocle"); + System.setProperty("monocle.headless.update", "false"); + System.setProperty("monocle.keyboard", "null"); + System.setProperty("monocle.mouse", "null"); } // Collect all FXML file resource paths in the view package @@ -37,12 +46,6 @@ static Stream fxmlPaths() throws IOException { .map(p -> "/com/starlight/view/" + p.getFileName().toString()); } - @Override - public void start(Stage stage) { - // Not needed for FXML loading tests, but required by ApplicationTest - // You can use stage.setScene(new Scene(...)) if you want to show nodes. - } - @ParameterizedTest @MethodSource("fxmlPaths") void testLoadFXMLWithLoader(String path) throws Exception { diff --git a/src/test/java/com/starlight/util/FXMLVerificatorTest.java b/src/test/java/com/starlight/util/FXMLVerificatorTest.java new file mode 100644 index 0000000..1f59644 --- /dev/null +++ b/src/test/java/com/starlight/util/FXMLVerificatorTest.java @@ -0,0 +1,218 @@ +package com.starlight.util; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link FXMLVerificator} utility class. + */ +public class FXMLVerificatorTest { + private Path tempDir; + private Path tempFxmlFile; + + @BeforeEach + void setUp() throws IOException { + tempDir = Files.createTempDirectory("fxml-test"); + tempFxmlFile = tempDir.resolve("test.fxml"); + } + + @AfterEach + void tearDown() throws IOException { + Files.deleteIfExists(tempFxmlFile); + Files.deleteIfExists(tempDir); + } + + @Test + void testVerifyAllWithNoFxmlFiles() { + // Test with empty directory - should not throw exception + assertDoesNotThrow(() -> FXMLVerificator.verifyAll()); + } + + @Test + void testFxmlWithCorrectNamespace() throws IOException { + String validFxml = "\n" + + "\n" + + " \n" + + " \n" + + ""; + + Files.writeString(tempFxmlFile, validFxml); + + // This should not throw any exceptions + assertDoesNotThrow(() -> FXMLVerificator.verifyAll()); + } + + @Test + void testFxmlWithIncorrectNamespace() throws IOException { + String fxmlWithOldNamespace = "\n" + + "\n" + + " \n" + + " \n" + + ""; + + Files.writeString(tempFxmlFile, fxmlWithOldNamespace); + + // Should process without throwing exceptions (it should normalize the namespace) + assertDoesNotThrow(() -> FXMLVerificator.verifyAll()); + } + + @Test + void testFxmlWithIllegalAttribute() throws IOException { + // Create a temporary FXML file in the resources directory + Path resourcesDir = Paths.get("src/main/resources"); + Files.createDirectories(resourcesDir); + Path testFxmlFile = resourcesDir.resolve("test_illegal.fxml"); + + try { + String fxmlWithIllegalAttr = "\n" + + "\n" + + " \n" + + " \n" + + ""; + + Files.writeString(testFxmlFile, fxmlWithIllegalAttr); + + // Should throw RuntimeException due to illegal attribute + assertThrows(RuntimeException.class, () -> FXMLVerificator.verifyAll()); + } finally { + // Clean up + Files.deleteIfExists(testFxmlFile); + } + } + + @Test + void testFxmlWithUnsupportedTag() throws IOException { + // Create a temporary FXML file in the resources directory + Path resourcesDir = Paths.get("src/main/resources"); + Files.createDirectories(resourcesDir); + Path testFxmlFile = resourcesDir.resolve("test_unsupported.fxml"); + + try { + String fxmlWithUnsupportedTag = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + Files.writeString(testFxmlFile, fxmlWithUnsupportedTag); + + // Should throw RuntimeException due to unsupported tag + assertThrows(RuntimeException.class, () -> FXMLVerificator.verifyAll()); + } finally { + // Clean up + Files.deleteIfExists(testFxmlFile); + } + } + + @Test + void testFxmlWithCustomComponents() throws IOException { + String fxmlWithCustom = "\n" + + "\n" + + " \n" + + " + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- - + From aa74e253a4cd697137f9bd9f2f25bcd75c1befe1 Mon Sep 17 00:00:00 2001 From: Gnockz11 Date: Fri, 18 Jul 2025 00:16:57 +0700 Subject: [PATCH 20/45] Update FXML Mission 1.0 --- .../resources/com/starlight/view/mission.fxml | 458 +++++++++++++++++- 1 file changed, 444 insertions(+), 14 deletions(-) diff --git a/src/main/resources/com/starlight/view/mission.fxml b/src/main/resources/com/starlight/view/mission.fxml index 139a51a..5c41acd 100644 --- a/src/main/resources/com/starlight/view/mission.fxml +++ b/src/main/resources/com/starlight/view/mission.fxml @@ -14,9 +14,9 @@ - + - + @@ -26,21 +26,31 @@ - + - + - + - - - - + + + + + + + + + + + + + + @@ -108,17 +118,19 @@ - +
- + - + - - + + + + @@ -156,10 +168,428 @@ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
From fcea333156ee8f25720ec1cafcd62c484be2be67 Mon Sep 17 00:00:00 2001 From: ZanDev32 <52073144+ZanDev32@users.noreply.github.com> Date: Fri, 18 Jul 2025 01:02:19 +0700 Subject: [PATCH 21/45] minor fix --- pom.xml | 105 ++++++++++-------- .../com/starlight/view/splashScreen.fxml | 4 +- 2 files changed, 60 insertions(+), 49 deletions(-) diff --git a/pom.xml b/pom.xml index 50b4bcb..47eac7d 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,16 @@ javafx-web 19 + + org.openjfx + javafx-base + 19 + + + org.openjfx + javafx-graphics + 19 + @@ -47,6 +57,14 @@ org.openjfx javafx-web + + org.openjfx + javafx-base + + + org.openjfx + javafx-graphics + io.github.palexdev materialfx @@ -188,40 +206,6 @@ - - org.apache.maven.plugins - maven-jar-plugin - 3.3.0 - - - - com.starlight.App - true - lib/ - - - - - - org.apache.maven.plugins - maven-dependency-plugin - 3.6.0 - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/lib - false - false - true - - - - org.apache.maven.plugins maven-shade-plugin @@ -233,13 +217,15 @@ shade - true - fat + false + false - com.starlight.App + com.starlight.Launcher - javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED javafx.graphics/com.sun.javafx.application=ALL-UNNAMED + true + javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED javafx.graphics/com.sun.javafx.application=ALL-UNNAMED javafx.graphics/com.sun.glass.ui=ALL-UNNAMED javafx.graphics/com.sun.javafx.util=ALL-UNNAMED javafx.graphics/com.sun.glass.ui.monocle=ALL-UNNAMED javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED javafx.graphics/com.sun.javafx.application.HostServicesDelegate=ALL-UNNAMED + javafx.graphics/com.sun.javafx.application=ALL-UNNAMED javafx.graphics/com.sun.glass.ui=ALL-UNNAMED javafx.graphics/com.sun.javafx.util=ALL-UNNAMED javafx.base/com.sun.javafx.logging=ALL-UNNAMED javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED javafx.graphics/com.sun.glass.ui.monocle=ALL-UNNAMED javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED javafx.graphics/com.sun.glass.ui.monocle.input=ALL-UNNAMED javafx.graphics/com.sun.glass.ui.monocle.input.devices=ALL-UNNAMED @@ -251,6 +237,8 @@ META-INF/*.SF META-INF/*.DSA META-INF/*.RSA + module-info.class + META-INF/versions/*/module-info.class @@ -259,16 +247,39 @@ - org.panteleyev - jpackage-maven-plugin - 1.6.0 + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + package + + run + + + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 - TabemonPal - ${project.version} - Starlight - target/dist - com.starlight/com.starlight.App - target/image + com.starlight.App + + --add-modules + javafx.controls,javafx.fxml,javafx.web + --add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED + --add-exports=javafx.graphics/com.sun.glass.ui=ALL-UNNAMED + --add-opens=javafx.graphics/com.sun.glass.ui=ALL-UNNAMED + --add-exports=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED + --add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED + diff --git a/src/main/resources/com/starlight/view/splashScreen.fxml b/src/main/resources/com/starlight/view/splashScreen.fxml index 81292f8..b15cfc6 100644 --- a/src/main/resources/com/starlight/view/splashScreen.fxml +++ b/src/main/resources/com/starlight/view/splashScreen.fxml @@ -26,7 +26,7 @@ - + @@ -40,6 +40,6 @@ From 6b51855c8b944c22d1d4a0b10b1b93f1836a2aaa Mon Sep 17 00:00:00 2001 From: ZanDev32 <52073144+ZanDev32@users.noreply.github.com> Date: Fri, 18 Jul 2025 21:05:14 +0700 Subject: [PATCH 22/45] Refactor UI components and enhance functionality - Updated CSS styles for post creation and interaction buttons to improve UI consistency. - Modified FXML files for community, createPost, postItem, and recipeItem views to align with new styles and structure. - Implemented new DeleteDialogController and EditPostController for handling post deletion and editing functionalities. - Added XML data structure for dummy post data to support testing and UI rendering. - Enhanced Post model to include comment count and liked state, with corresponding unit tests to validate functionality. - Created tests for PostDataRepository to ensure proper saving and loading of post liked states. - Added icons for like and photo functionalities to improve user experience. --- .../controller/CommunityController.java | 33 +++- .../controller/CreatePostController.java | 6 +- .../controller/DeleteDialogController.java | 19 +++ .../controller/EditPostController.java | 61 ++++++++ .../controller/PostItemController.java | 146 +++++++++++++++++- .../controller/ProfileController.java | 2 +- src/main/java/com/starlight/models/Post.java | 4 + .../com/starlight/models/PostDataDummy.xml | 77 +++++++++ .../starlight/models/PostDataRepository.java | 21 ++- .../com/starlight/util/FileSystemManager.java | 17 +- .../resources/com/starlight/icon/like_1.png | Bin 0 -> 467 bytes .../resources/com/starlight/icon/photo.png | Bin 0 -> 526 bytes src/main/resources/com/starlight/style.css | 53 ++++++- .../com/starlight/view/community.fxml | 28 +++- .../com/starlight/view/createPost.fxml | 79 ++++++---- .../com/starlight/view/deleteDialog.fxml | 34 ++++ .../com/starlight/view/editPost.fxml | 109 +++++++++++++ .../com/starlight/view/postItem.fxml | 4 +- .../com/starlight/view/recipeItem.fxml | 8 +- .../models/PostDataRepositoryTest.java | 4 + .../starlight/models/PostLikedStateTest.java | 129 ++++++++++++++++ .../java/com/starlight/models/PostTest.java | 32 ++++ 22 files changed, 794 insertions(+), 72 deletions(-) create mode 100644 src/main/java/com/starlight/controller/DeleteDialogController.java create mode 100644 src/main/java/com/starlight/controller/EditPostController.java create mode 100644 src/main/java/com/starlight/models/PostDataDummy.xml create mode 100644 src/main/resources/com/starlight/icon/like_1.png create mode 100644 src/main/resources/com/starlight/icon/photo.png create mode 100644 src/main/resources/com/starlight/view/deleteDialog.fxml create mode 100644 src/main/resources/com/starlight/view/editPost.fxml create mode 100644 src/test/java/com/starlight/models/PostLikedStateTest.java diff --git a/src/main/java/com/starlight/controller/CommunityController.java b/src/main/java/com/starlight/controller/CommunityController.java index 37731b8..09bd022 100644 --- a/src/main/java/com/starlight/controller/CommunityController.java +++ b/src/main/java/com/starlight/controller/CommunityController.java @@ -9,6 +9,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; +import java.util.Collections; import com.starlight.models.Post; import com.starlight.models.PostDataRepository; @@ -228,6 +229,18 @@ private void loadPosts() { c.uploadtime.setText(formatRelativeTime(time)); c.description.setText(desc); c.likecounter.setText(likes); + + // Set comment count (default to 0 if not set) + String commentCount = p.commentcount != null ? p.commentcount : "0"; + c.commentcounter.setText(commentCount); + + // Set the post data for the controller + c.setPost(p); + + // Initialize isLiked field if not set + if (p.isLiked == null) { + p.isLiked = "false"; + } // Load profile picture loadImage(c.profile1, pp, "/com/starlight/images/missing.png"); @@ -245,7 +258,7 @@ private void loadPosts() { } /** - * Loads the top posts into the daily post slots with relative time formatting + * Loads the top posts into the daily post slots with random selection */ private void loadDailyPosts() { List posts = repository.loadPosts(); @@ -253,37 +266,41 @@ private void loadDailyPosts() { return; } - // Take up to 4 posts for daily posts - for (int i = 0; i < Math.min(4, posts.size()); i++) { - Post post = posts.get(i); + // Create a copy of the posts list and shuffle it randomly + List shuffledPosts = new java.util.ArrayList<>(posts); + Collections.shuffle(shuffledPosts); + + // Take up to 4 posts for daily posts from the shuffled list + for (int i = 0; i < Math.min(4, shuffledPosts.size()); i++) { + Post post = shuffledPosts.get(i); switch (i) { case 0: dailytitle1.setText(post.title); starrating.setText(post.rating); dailylikecounter.setText(post.likecount); loadImage(dailyphoto1, post.image, "/com/starlight/images/missing.png"); - scaleToFit(dailyphoto1, 280, 200, 30); + scaleToFit(dailyphoto1, 280, 174, 30); break; case 1: dailytitle2.setText(post.title); starrating2.setText(post.rating); dailylikecounter2.setText(post.likecount); loadImage(dailyphoto2, post.image, "/com/starlight/images/missing.png"); - scaleToFit(dailyphoto2, 280, 200, 30); + scaleToFit(dailyphoto2, 280, 174, 30); break; case 2: dailytitle3.setText(post.title); starrating3.setText(post.rating); dailylikecounter3.setText(post.likecount); loadImage(dailyphoto3, post.image, "/com/starlight/images/missing.png"); - scaleToFit(dailyphoto3, 280, 200, 30); + scaleToFit(dailyphoto3, 280, 174, 30); break; case 3: dailytitle4.setText(post.title); starrating4.setText(post.rating); dailylikecounter4.setText(post.likecount); loadImage(dailyphoto4, post.image, "/com/starlight/images/missing.png"); - scaleToFit(dailyphoto4, 280, 200, 30); + scaleToFit(dailyphoto4, 280, 174, 30); break; } } diff --git a/src/main/java/com/starlight/controller/CreatePostController.java b/src/main/java/com/starlight/controller/CreatePostController.java index a9010eb..99c2105 100644 --- a/src/main/java/com/starlight/controller/CreatePostController.java +++ b/src/main/java/com/starlight/controller/CreatePostController.java @@ -6,6 +6,7 @@ import java.time.format.DateTimeFormatter; import java.util.ResourceBundle; import java.util.List; +import java.util.UUID; import com.starlight.models.Post; import com.starlight.models.PostDataRepository; @@ -119,11 +120,14 @@ public void initialize(URL url, ResourceBundle rb) { // Create a new Post object Post newPost = new Post(); + newPost.uuid = UUID.randomUUID().toString(); newPost.username = Session.getCurrentUser().username; newPost.profilepicture = "src/main/resources/com/starlight/images/dummy/2.png"; - newPost.uploadtime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); newPost.title = postTitle; newPost.description = postDescription; + newPost.ingredients = postIngredients; + newPost.directions = postDirections; + newPost.uploadtime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); try { String storedPath = copyImageToUserDir(selectedImage); newPost.image = storedPath != null ? storedPath : selectedImage.getAbsolutePath(); diff --git a/src/main/java/com/starlight/controller/DeleteDialogController.java b/src/main/java/com/starlight/controller/DeleteDialogController.java new file mode 100644 index 0000000..810b962 --- /dev/null +++ b/src/main/java/com/starlight/controller/DeleteDialogController.java @@ -0,0 +1,19 @@ +package com.starlight.controller; + +import io.github.palexdev.materialfx.controls.MFXButton; +import javafx.fxml.FXML; + +public class DeleteDialogController { + @FXML + private MFXButton delete; + + @FXML + private MFXButton cancel; + + + + @FXML + private void initialize() { + // Hint: initialize() will be called when the associated FXML has been completely loaded. + } +} diff --git a/src/main/java/com/starlight/controller/EditPostController.java b/src/main/java/com/starlight/controller/EditPostController.java new file mode 100644 index 0000000..0c0108f --- /dev/null +++ b/src/main/java/com/starlight/controller/EditPostController.java @@ -0,0 +1,61 @@ +package com.starlight.controller; + +import java.io.File; +import java.net.URL; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ResourceBundle; +import java.util.List; +import java.util.UUID; + +import com.starlight.models.Post; +import com.starlight.models.PostDataRepository; +import com.starlight.util.Session; + +import io.github.palexdev.materialfx.controls.MFXButton; +import io.github.palexdev.materialfx.controls.MFXTextField; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import javafx.scene.Node; + + +/** + * Controller responsible for the "edit post" dialog. + */ +public class EditPostController implements Initializable { + + @FXML + private MFXTextField title; + + @FXML + private MFXButton imagepicker; + + @FXML + private Label pickerstatus; + + @FXML + private MFXTextField description; + + @FXML + private TextArea ingredients; + + @FXML + private TextArea directions; + + @FXML + private MFXButton submit; + + @FXML + private MFXButton cancel; + + + @Override + public void initialize(URL location, ResourceBundle resources) { + // TODO Auto-generated method stub + + } +} \ No newline at end of file diff --git a/src/main/java/com/starlight/controller/PostItemController.java b/src/main/java/com/starlight/controller/PostItemController.java index 7a239b1..c739311 100644 --- a/src/main/java/com/starlight/controller/PostItemController.java +++ b/src/main/java/com/starlight/controller/PostItemController.java @@ -3,8 +3,12 @@ import io.github.palexdev.materialfx.controls.MFXButton; import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; +import com.starlight.models.Post; +import com.starlight.models.PostDataRepository; +import java.util.List; /** * Simple controller used for dynamically loaded post items. @@ -17,10 +21,10 @@ public class PostItemController { private VBox post1; @FXML - private ImageView likebutton; + public ImageView likebutton; @FXML - private MFXButton commentcounter; + public MFXButton commentcounter; @FXML private ImageView likebutton1; @@ -46,8 +50,144 @@ public class PostItemController { @FXML public MFXButton likecounter; + private Post currentPost; + private PostDataRepository repository = new PostDataRepository(); + @FXML private void initialize() { - // Hint: initialize() will be called when the associated FXML has been completely loaded. + // FXML initialize method - minimal setup only + } + + /** + * FXML binding method for like button click + */ + @FXML + private void handleLikeClick() { + if (currentPost == null) return; + + try { + int currentLikes = Integer.parseInt(currentPost.likecount != null ? currentPost.likecount : "0"); + boolean wasLiked = "true".equals(currentPost.isLiked); + + if (wasLiked) { + // Unlike the post + currentLikes = Math.max(0, currentLikes - 1); + currentPost.likecount = String.valueOf(currentLikes); + currentPost.isLiked = "false"; + + // Change image back to normal like icon + if (likebutton != null) { + likebutton.setImage(new Image(getClass().getResourceAsStream("/com/starlight/icon/like.png"))); + } + } else { + // Like the post + currentLikes++; + currentPost.likecount = String.valueOf(currentLikes); + currentPost.isLiked = "true"; + + // Change image to liked icon + if (likebutton != null) { + likebutton.setImage(new Image(getClass().getResourceAsStream("/com/starlight/icon/like_1.png"))); + } + } + + // Update the button text + likecounter.setText(String.valueOf(currentLikes)); + + // Save the updated post data + savePostData(); + + } catch (NumberFormatException e) { + // Handle invalid like count + currentPost.likecount = "0"; + currentPost.isLiked = "false"; + likecounter.setText("0"); + } + } + + /** + * FXML binding method for comment button click + */ + @FXML + private void handleCommentClick() { + if (currentPost == null) return; + + try { + int currentComments = Integer.parseInt(currentPost.commentcount != null ? currentPost.commentcount : "0"); + currentComments++; + currentPost.commentcount = String.valueOf(currentComments); + + // Update the button text + commentcounter.setText(String.valueOf(currentComments)); + + // Save the updated post data + savePostData(); + + } catch (NumberFormatException e) { + // Handle invalid comment count + currentPost.commentcount = "0"; + commentcounter.setText("0"); + } + } + + /** + * Sets the current post data for this controller instance + */ + public void setPost(Post post) { + this.currentPost = post; + updateUIFromPost(); + } + + /** + * Updates the UI elements with the current post data + */ + private void updateUIFromPost() { + if (currentPost == null) return; + + // Update like counter + if (likecounter != null) { + likecounter.setText(currentPost.likecount != null ? currentPost.likecount : "0"); + } + + // Update comment counter + if (commentcounter != null) { + commentcounter.setText(currentPost.commentcount != null ? currentPost.commentcount : "0"); + } + + // Update like button image based on liked state + if (likebutton != null) { + boolean isLiked = "true".equals(currentPost.isLiked); + if (isLiked) { + likebutton.setImage(new Image(getClass().getResourceAsStream("/com/starlight/icon/like_1.png"))); + } else { + likebutton.setImage(new Image(getClass().getResourceAsStream("/com/starlight/icon/like.png"))); + } + } + } + + /** + * Saves the updated post data to the repository + */ + private void savePostData() { + if (currentPost == null) return; + + try { + List posts = repository.loadPosts(); + + // Find and update the current post in the list + for (int i = 0; i < posts.size(); i++) { + Post post = posts.get(i); + if (post.uuid != null && post.uuid.equals(currentPost.uuid)) { + posts.set(i, currentPost); + break; + } + } + + // Save the updated posts + repository.savePosts(posts); + + } catch (Exception e) { + e.printStackTrace(); + } } } diff --git a/src/main/java/com/starlight/controller/ProfileController.java b/src/main/java/com/starlight/controller/ProfileController.java index 8ba77ee..44fbcd8 100644 --- a/src/main/java/com/starlight/controller/ProfileController.java +++ b/src/main/java/com/starlight/controller/ProfileController.java @@ -183,7 +183,7 @@ private void loadUserRecipes() { ImageView recipeImageView = controller.getImageView(); if (recipeImageView != null) { communityController.loadImage(recipeImageView, image, "/com/starlight/images/missing.png"); - communityController.scaleToFit(recipeImageView, 280, 200, 20); // Adjust size for recipe item + communityController.scaleToFit(recipeImageView, 280, 174, 20); // Adjust size for recipe item } recipeContainer.getChildren().add(recipeNode); diff --git a/src/main/java/com/starlight/models/Post.java b/src/main/java/com/starlight/models/Post.java index 2bb5979..428cdc1 100644 --- a/src/main/java/com/starlight/models/Post.java +++ b/src/main/java/com/starlight/models/Post.java @@ -26,4 +26,8 @@ public class Post { public String uploadtime; /** Like count. */ public String likecount; + /** Comment count. */ + public String commentcount; + /** Whether the current user has liked this post. */ + public String isLiked; } diff --git a/src/main/java/com/starlight/models/PostDataDummy.xml b/src/main/java/com/starlight/models/PostDataDummy.xml new file mode 100644 index 0000000..bb085ac --- /dev/null +++ b/src/main/java/com/starlight/models/PostDataDummy.xml @@ -0,0 +1,77 @@ + + + 550e8400-e29b-41d4-a716-446655440001 + gw + src/main/resources/com/starlight/images/dummy/profileman.jpg + Delicious Spaghetti Carbonara + A classic Italian pasta dish with eggs, cheese, and pancetta. Simple yet elegant! + 400g spaghetti, 200g pancetta, 4 eggs, 100g Pecorino Romano, black pepper + 1. Cook pasta al dente. 2. Fry pancetta until crispy. 3. Mix eggs and cheese. 4. Combine everything off heat. + src/main/resources/com/starlight/images/dummy/carbonara.jpg + 4.8 + 2025-07-15 14:30:00 + 127 + 23 + false + + + 550e8400-e29b-41d4-a716-446655440002 + M. Fakih Sahman + src/main/resources/com/starlight/images/dummy/profileman2.jpg + Healthy Quinoa Buddha Bowl + Nutritious and colorful bowl packed with vegetables, quinoa, and tahini dressing. + 1 cup quinoa, mixed vegetables, chickpeas, tahini, lemon juice, olive oil + 1. Cook quinoa. 2. Roast vegetables. 3. Mix tahini dressing. 4. Assemble bowl. + src/main/resources/com/starlight/images/dummy/buddha_bowl.jpg + 4.6 + 2025-07-14 09:15:00 + 89 + 15 + false + + + 550e8400-e29b-41d4-a716-446655440003 + Khairun Nissa + src/main/resources/com/starlight/images/dummy/profilewoman.jpg + Homemade Chocolate Chip Cookies + Soft, chewy chocolate chip cookies that are perfect for any occasion! + 2 cups flour, 1 cup butter, 1 cup brown sugar, 2 eggs, chocolate chips + 1. Cream butter and sugar. 2. Add eggs and flour. 3. Fold in chocolate chips. 4. Bake at 375Β°F. + src/main/resources/com/starlight/images/dummy/cookies.jpg + 4.9 + 2025-07-13 16:45:00 + 203 + 31 + false + + + 550e8400-e29b-41d4-a716-446655440004 + John Doe + src/main/resources/com/starlight/images/dummy/profileman3.jpg + Grilled Salmon with Lemon + Fresh salmon grilled to perfection with a zesty lemon marinade. + 4 salmon fillets, 2 lemons, olive oil, garlic, herbs + 1. Marinate salmon in lemon and herbs. 2. Preheat grill. 3. Grill for 6-8 minutes per side. + src/main/resources/com/starlight/images/dummy/salmon.jpg + 4.7 + 2025-07-12 18:20:00 + 156 + 19 + false + + + 550e8400-e29b-41d4-a716-446655440005 + gw + src/main/resources/com/starlight/images/dummy/profileman.jpg + Vegetarian Tacos + Delicious plant-based tacos with seasoned black beans and fresh vegetables. + Black beans, corn tortillas, avocado, tomatoes, onions, cilantro, lime + 1. Heat tortillas. 2. Season black beans. 3. Prepare toppings. 4. Assemble tacos. + src/main/resources/com/starlight/images/dummy/tacos.jpg + 4.5 + 2025-07-11 12:00:00 + 78 + 12 + false + + diff --git a/src/main/java/com/starlight/models/PostDataRepository.java b/src/main/java/com/starlight/models/PostDataRepository.java index 1ee0608..d39b5b9 100644 --- a/src/main/java/com/starlight/models/PostDataRepository.java +++ b/src/main/java/com/starlight/models/PostDataRepository.java @@ -72,7 +72,17 @@ private List loadPosts(boolean useDummy, boolean fallbackToDummyIfMissing) try (FileInputStream fis = new FileInputStream(xmlFile)) { Object obj = xstream.fromXML(fis); if (obj instanceof List) { - return (List) obj; + List posts = (List) obj; + // Initialize missing fields for existing posts + for (Post post : posts) { + if (post.commentcount == null) { + post.commentcount = "0"; + } + if (post.isLiked == null) { + post.isLiked = "false"; + } + } + return posts; } } catch (Exception e) { e.printStackTrace(); @@ -104,6 +114,15 @@ public void ensureDummyData() { */ public void savePosts(List posts) { try (FileOutputStream fos = new FileOutputStream(xmlPath)) { + // Ensure all posts have the required fields before saving + for (Post post : posts) { + if (post.commentcount == null) { + post.commentcount = "0"; + } + if (post.isLiked == null) { + post.isLiked = "false"; + } + } xstream.toXML(posts, fos); } catch (Exception e) { throw new RuntimeException("Failed to save posts", e); diff --git a/src/main/java/com/starlight/util/FileSystemManager.java b/src/main/java/com/starlight/util/FileSystemManager.java index e7ed312..ee05e15 100644 --- a/src/main/java/com/starlight/util/FileSystemManager.java +++ b/src/main/java/com/starlight/util/FileSystemManager.java @@ -296,20 +296,11 @@ public static Path resolveImagePath(String imagePath) { * @return path to a fallback image, or null if no fallback available */ public static String getFallbackImagePath(String imageType) { - String[] fallbackPaths = { - "src/main/resources/com/starlight/images/dummy/profileman.jpg", - "src/main/resources/com/starlight/images/dummy/2.png", - "src/main/resources/com/starlight/images/dummy/recent_1.png", - "src/main/resources/com/starlight/images/dummy/image_2.jpg" - }; - - for (String fallbackPath : fallbackPaths) { - Path resolvedPath = resolveImagePath(fallbackPath); - if (resolvedPath != null) { - return resolvedPath.toString(); - } + String fallbackPath = "src/main/resources/com/starlight/images/missing.png"; + Path resolvedPath = resolveImagePath(fallbackPath); + if (resolvedPath != null) { + return resolvedPath.toString(); } - return null; } diff --git a/src/main/resources/com/starlight/icon/like_1.png b/src/main/resources/com/starlight/icon/like_1.png new file mode 100644 index 0000000000000000000000000000000000000000..2dbda0faa21b97227b24048d35a55bd31711e0c6 GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?2=RS;(M3{v?36l5$8 za(7}_cTVOdki(Mh=w zV=;SR_JSi+eSvfet67G*g7gx*tL>LM{v>t8PxY_z;CTIK?$3?74O6Giu9#&|CcTNf z(9lb?sB5JG@7m|dYjc~b=Kh#q^h74;yUTGAF@i0NQT-SIG+Bwll{8kqoOGpL1|i5y7giA3ncb^eUjduEx?Sz_8_$w~68oVWtnK>=$t!Y!(nWEzOiv>Fl7B z&(ZOemvQ4BMTKeREJr@IHALJ4NjBZz@9}k>Z3S;kWMqeYWO&YZzFX@+vBco%>gTe~ HDWM4f%>laV literal 0 HcmV?d00001 diff --git a/src/main/resources/com/starlight/icon/photo.png b/src/main/resources/com/starlight/icon/photo.png new file mode 100644 index 0000000000000000000000000000000000000000..0090c714c5cf42ff75aa3de1a3cd630a5732dbe3 GIT binary patch literal 526 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?2=RS;(M3{v?36l5$8 za(7}_cTVOdki(Mh=j@0?$%GsW%0H!?a3u*_#! zckW}vN{d5HF5gRLEz0NGQ&ab_a`VMmlQ*#WeDrJy|Do|`0@ExXn<6!*cUD?;jQ5X9 zGvx#yC~fcaUM|70n@u3$L`L1WX6tZ-(kM3DP z*^8?iFPawpQk&nmZo*<^E5m~6DWb5TL>lJaep)l#BrJUDK0TnI&urK4|JbvpfBx%V?9sYUua`Gs=C=1c z&YQ1J{NkIDvOw$dHDAs9lQ+)VWs+KZ`DeiG2Ib4sbUOO7bRazA%efgA@_zTkf3<#V ze^xRhS>eR?3Hf^W7_8Zw801|)e%BAWwOWSf4M)JoY#x@XV(oSPrUI%nW(PK;`|uVv zsjr)Fx?pQuyP@2Jf_lF){ruFd1yLp}jsk2a%}cpLX3k4>-vo?422WQ%mvv4FO#oom B*Yp4Y literal 0 HcmV?d00001 diff --git a/src/main/resources/com/starlight/style.css b/src/main/resources/com/starlight/style.css index 673d8ad..d0c0c31 100644 --- a/src/main/resources/com/starlight/style.css +++ b/src/main/resources/com/starlight/style.css @@ -93,22 +93,38 @@ font-weight: 100; } -.create-post { +.Popup-post { -fx-background-color: #FEF7E7; background-color: #FEF7E7; } -.create-post-header { +.Popup-post-title { + -fx-font-size: 20; + font-size: 20; +} +.Popup-post-header { -fx-font-size: 13; font-size: 13; } -.create-post-desc { +.Popup-post-desc { -fx-font-size: 7; font-size: 7; } +.Popup-image { + -fx-background-color: #f5f5f5; + background-color: #f5f5f5; + -fx-background-radius: 20 20 20 20; + -fx-padding: 10px; + padding: 10px; + -fx-border-color: #EED8AF; + border-color: #EED8AF; + -fx-border-radius: 10 10 10 10; + border-radius: 10 10 10 10; +} + .interaction-button { -fx-font-family: Poppons; font-family: Poppons; @@ -160,6 +176,37 @@ -fx-background-radius: 30 30 30 30; } +.delete-button { + -fx-background-color: #FF6B6B; + background-color: #FF6B6B; + -fx-background-radius: 20 20 20 20; + -fx-text-fill: white; + -fx-font-family: Poppins; + font-family: Poppins; + -fx-font-style: normal; + font-style: normal; + -fx-font-size: 15; + font-size: 15; +} + +.normal-button { + -fx-background-color: #EED8AF; + background-color: #EED8AF; + -fx-background-radius: 20 20 20 20; + -fx-text-fill: #5b543f; + -fx-font-family: Poppins; + font-family: Poppins; + -fx-font-style: normal; + font-style: normal; + -fx-font-size: 15; + font-size: 15; + -fx-background-radius: 25 25 25 25; + -fx-background-radius: 25 25 25 25; + -fx-border-radius: 25 25 25 25; + border-radius: 25 25 25 25; +} + + /* debugging only .VBox { diff --git a/src/main/resources/com/starlight/view/community.fxml b/src/main/resources/com/starlight/view/community.fxml index 8d36653..760aa1b 100644 --- a/src/main/resources/com/starlight/view/community.fxml +++ b/src/main/resources/com/starlight/view/community.fxml @@ -287,7 +287,7 @@ -
- + @@ -84,7 +84,7 @@ - + diff --git a/src/main/resources/com/starlight/view/recipeItem.fxml b/src/main/resources/com/starlight/view/recipeItem.fxml index 644b733..ba34142 100644 --- a/src/main/resources/com/starlight/view/recipeItem.fxml +++ b/src/main/resources/com/starlight/view/recipeItem.fxml @@ -18,11 +18,11 @@ - - + + - + @@ -64,7 +64,7 @@ - + + -
+ diff --git a/src/test/java/com/starlight/api/ChatbotAPITest.java b/src/test/java/com/starlight/api/ChatbotAPITest.java new file mode 100644 index 0000000..a131140 --- /dev/null +++ b/src/test/java/com/starlight/api/ChatbotAPITest.java @@ -0,0 +1,124 @@ +package com.starlight.api; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for ChatbotAPI. + */ +public class ChatbotAPITest { + + @BeforeEach + public void setUp() { + // Note: This test won't actually create the ChatbotAPI since it requires a valid API key + // We'll test the structure and error handling instead + } + + @Test + public void testChatbotExceptionCreation() { + // Test exception creation + ChatbotAPI.ChatbotException exception = new ChatbotAPI.ChatbotException("Test message"); + assertEquals("Test message", exception.getMessage()); + + Exception cause = new RuntimeException("Cause"); + ChatbotAPI.ChatbotException exceptionWithCause = new ChatbotAPI.ChatbotException("Test message", cause); + assertEquals("Test message", exceptionWithCause.getMessage()); + assertEquals(cause, exceptionWithCause.getCause()); + } + + @Test + public void testConfigCreation() { + // Test config object creation + ChatbotAPI.Config config = new ChatbotAPI.Config(); + assertNull(config.getOpenaiKey()); + + config.setOpenaiKey("test-key"); + assertEquals("test-key", config.getOpenaiKey()); + } + + @Test + public void testChatbotInitializationWithoutValidKey() { + // The ChatbotAPI should initialize successfully + ChatbotAPI chatbot = new ChatbotAPI(); + + // Test that it can be used (either returns demo message, API key error, or actual response) + try { + String response = chatbot.sendMessage("Hello"); + // If we get here, either demo mode is working or API key is configured + assertTrue(response.contains("demo mode") || + response.contains("configure your OpenAI API key") || + response.length() > 0); // Any non-empty response is acceptable + } catch (ChatbotAPI.ChatbotException e) { + // This is also acceptable - it means the API key is missing or there's an error + assertTrue(e.getMessage().contains("demo mode") || + e.getMessage().contains("API key") || + e.getMessage().length() > 0); // Any error message is acceptable + } + } + + @Test + public void testJsonResponseParsing() throws ChatbotAPI.ChatbotException { + // Test the improved JSON parsing with a mock OpenAI API response + ChatbotAPI chatbot = new ChatbotAPI(); + + // Mock OpenAI API response format + String mockResponse = "{\n" + + " \"id\": \"chatcmpl-123\",\n" + + " \"object\": \"chat.completion\",\n" + + " \"created\": 1677652288,\n" + + " \"model\": \"gpt-3.5-turbo\",\n" + + " \"choices\": [\n" + + " {\n" + + " \"index\": 0,\n" + + " \"message\": {\n" + + " \"role\": \"assistant\",\n" + + " \"content\": \"Hello! How can I help you today?\"\n" + + " },\n" + + " \"finish_reason\": \"stop\"\n" + + " }\n" + + " ],\n" + + " \"usage\": {\n" + + " \"prompt_tokens\": 10,\n" + + " \"completion_tokens\": 9,\n" + + " \"total_tokens\": 19\n" + + " }\n" + + "}"; + + String result = chatbot.parseResponse(mockResponse); + assertEquals("Hello! How can I help you today?", result); + } + + @Test + public void testJsonResponseParsingWithEscapedCharacters() throws ChatbotAPI.ChatbotException { + // Test JSON parsing with escaped characters + ChatbotAPI chatbot = new ChatbotAPI(); + + String mockResponse = "{\n" + + " \"choices\": [\n" + + " {\n" + + " \"message\": {\n" + + " \"content\": \"Hello! I can help with \\\"nutrition\\\" and \\n food advice.\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + + String result = chatbot.parseResponse(mockResponse); + assertEquals("Hello! I can help with \"nutrition\" and \n food advice.", result); + } + + @Test + public void testJsonResponseParsingInvalidFormat() { + // Test error handling with invalid JSON format + ChatbotAPI chatbot = new ChatbotAPI(); + + String invalidResponse = "{\"invalid\": \"format\"}"; + + ChatbotAPI.ChatbotException exception = assertThrows(ChatbotAPI.ChatbotException.class, () -> { + chatbot.parseResponse(invalidResponse); + }); + + assertTrue(exception.getMessage().contains("Invalid response format")); + } +} diff --git a/src/test/java/com/starlight/controller/ConsultControllerTest.java b/src/test/java/com/starlight/controller/ConsultControllerTest.java new file mode 100644 index 0000000..6d89e00 --- /dev/null +++ b/src/test/java/com/starlight/controller/ConsultControllerTest.java @@ -0,0 +1,48 @@ +package com.starlight.controller; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import com.starlight.util.FileSystemManager; +import java.nio.file.Path; + +/** + * Test class for ConsultController avatar path resolution. + */ +public class ConsultControllerTest { + + @Test + public void testAvatarPathResolution() { + // Test that the avatar paths can be resolved by FileSystemManager + String botAvatarPath = "src/main/resources/com/starlight/images/dummy/profilewoman.jpg"; + String userAvatarPath = "src/main/resources/com/starlight/images/dummy/profilewoman.jpg"; + + // Test bot avatar path resolution + Path resolvedBotPath = FileSystemManager.resolveImagePath(botAvatarPath); + assertNotNull(resolvedBotPath, "Bot avatar path should be resolvable"); + + // Test user avatar path resolution + Path resolvedUserPath = FileSystemManager.resolveImagePath(userAvatarPath); + assertNotNull(resolvedUserPath, "User avatar path should be resolvable"); + + // Test fallback path resolution + String fallbackPath = "src/main/resources/com/starlight/images/missing.png"; + Path resolvedFallback = FileSystemManager.resolveImagePath(fallbackPath); + assertNotNull(resolvedFallback, "Fallback image path should be resolvable"); + } + + @Test + public void testResourcePathFormat() { + // Test that our path format works with the FileSystemManager + String resourcePath = "src/main/resources/com/starlight/images/dummy/profilewoman.jpg"; + + // Should not be null and should not generate warnings + Path resolved = FileSystemManager.resolveImagePath(resourcePath); + assertNotNull(resolved, "Resource path should be resolvable"); + + // Check that it's either a valid file path or a resource marker + assertTrue(resolved.toString().contains("profilewoman.jpg") || + resolved.toString().startsWith("resource:"), + "Resolved path should contain the image name or be a resource marker"); + } +} diff --git a/src/test/java/com/starlight/models/PostDataRepositoryTest.java b/src/test/java/com/starlight/models/PostDataRepositoryTest.java index 548c6af..dc790b1 100644 --- a/src/test/java/com/starlight/models/PostDataRepositoryTest.java +++ b/src/test/java/com/starlight/models/PostDataRepositoryTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.starlight.repository.PostDataRepository; + /** * Unit tests for {@link PostDataRepository}. */ diff --git a/src/test/java/com/starlight/models/PostLikedStateTest.java b/src/test/java/com/starlight/models/PostLikedStateTest.java index da6ad71..959a170 100644 --- a/src/test/java/com/starlight/models/PostLikedStateTest.java +++ b/src/test/java/com/starlight/models/PostLikedStateTest.java @@ -1,6 +1,9 @@ package com.starlight.models; import org.junit.jupiter.api.Test; + +import com.starlight.repository.PostDataRepository; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.AfterEach; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/test/java/com/starlight/models/UserDataRepositoryTest.java b/src/test/java/com/starlight/models/UserDataRepositoryTest.java index 218ac50..29bd532 100644 --- a/src/test/java/com/starlight/models/UserDataRepositoryTest.java +++ b/src/test/java/com/starlight/models/UserDataRepositoryTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.starlight.repository.UserDataRepository; + /** * Unit tests for {@link UserDataRepository}. */ diff --git a/src/test/java/com/starlight/util/DataRepositoryIntegrationTest.java b/src/test/java/com/starlight/util/DataRepositoryIntegrationTest.java index 4b09b54..7f4f7d7 100644 --- a/src/test/java/com/starlight/util/DataRepositoryIntegrationTest.java +++ b/src/test/java/com/starlight/util/DataRepositoryIntegrationTest.java @@ -4,8 +4,8 @@ import org.junit.jupiter.api.BeforeEach; import static org.junit.jupiter.api.Assertions.*; -import com.starlight.models.UserDataRepository; -import com.starlight.models.PostDataRepository; +import com.starlight.repository.PostDataRepository; +import com.starlight.repository.UserDataRepository; /** * Integration test to verify that data repositories work correctly with the new file system. From ec579115916284068a7b42f9cf8452fdea6bff9c Mon Sep 17 00:00:00 2001 From: ZanDev32 <52073144+ZanDev32@users.noreply.github.com> Date: Sat, 19 Jul 2025 07:08:00 +0700 Subject: [PATCH 27/45] minor fix --- .../java/com/starlight/api/ChatbotAPI.java | 3 +- .../controller/ConsultController.java | 142 +++++++++++------- .../com/starlight/images/botIcon.png | Bin 0 -> 20506 bytes .../resources/com/starlight/view/main.fxml | 4 +- 4 files changed, 88 insertions(+), 61 deletions(-) create mode 100644 src/main/resources/com/starlight/images/botIcon.png diff --git a/src/main/java/com/starlight/api/ChatbotAPI.java b/src/main/java/com/starlight/api/ChatbotAPI.java index 2fa0bc2..b14a909 100644 --- a/src/main/java/com/starlight/api/ChatbotAPI.java +++ b/src/main/java/com/starlight/api/ChatbotAPI.java @@ -150,7 +150,8 @@ public String askNutritionQuestion(String question) throws ChatbotException { String systemMessage = "You are Kuro , a helpful nutrition assistant for the TabemonPal app. " + "Provide accurate, helpful nutrition advice and food recommendations. " + "Keep your responses concise but informative. " + - "If you're unsure about medical advice, recommend consulting with a healthcare professional."; + "If you're unsure about medical advice, recommend consulting with a healthcare professional." + + "Avoid giving information on topics outside of nutrition, Health, or wellness."; return sendMessage(question, systemMessage); } diff --git a/src/main/java/com/starlight/controller/ConsultController.java b/src/main/java/com/starlight/controller/ConsultController.java index dcc521e..7298d02 100644 --- a/src/main/java/com/starlight/controller/ConsultController.java +++ b/src/main/java/com/starlight/controller/ConsultController.java @@ -13,8 +13,6 @@ import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Insets; @@ -28,7 +26,11 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; +import javafx.scene.text.TextFlow; /** * Controller for the consult view with chatbot functionality. @@ -54,7 +56,6 @@ public class ConsultController implements Initializable { // Properties for FXML binding private final BooleanProperty isProcessing = new SimpleBooleanProperty(false); - private final StringProperty attachmentStatus = new SimpleStringProperty("No attachment"); private final BooleanProperty hasTextInput = new SimpleBooleanProperty(false); public ConsultController() { @@ -142,8 +143,8 @@ private void addBubble(String message, boolean isUser) { // Create and configure avatar ImageView avatar = createAvatar(isUser); - // Create and configure text bubble - Label textBubble = createTextBubble(message, isUser); + // Create and configure text bubble using TextFlow + TextFlow textBubble = createTextBubble(message, isUser); // Add components to the row in correct order if (isUser) { @@ -180,43 +181,39 @@ private ImageView createAvatar(boolean isUser) { ImageUtils.loadImage(avatar, avatarPath, "src/main/resources/com/starlight/images/missing.png"); // Apply scaling and styling - ImageUtils.scaleToFit(avatar, 32, 32, 16); + ImageUtils.scaleToFit(avatar, 32, 32, 40); return avatar; } /** - * Creates and configures a text bubble Label for the chat message. + * Creates and configures a text bubble TextFlow for the chat message. * * @param message The message text to display * @param isUser True if this is a user message, false for bot message - * @return Configured Label for the text bubble + * @return Configured TextFlow for the text bubble */ - private Label createTextBubble(String message, boolean isUser) { - Label textBubble = new Label(message); + private TextFlow createTextBubble(String message, boolean isUser) { + TextFlow textBubble = new TextFlow(); - // Basic text configuration - textBubble.setWrapText(true); + // Basic configuration textBubble.setMaxWidth(640); textBubble.setPadding(new Insets(12, 15, 12, 15)); textBubble.setTextAlignment(TextAlignment.LEFT); + // Parse and add formatted text + parseAndAddFormattedText(textBubble, message, isUser); + // Apply styling based on sender if (isUser) { textBubble.setStyle( "-fx-background-color: #B8145B; " + - "-fx-background-radius: 18 5 18 18; " + - "-fx-font-size: 14px; " + - "-fx-font-family: 'Poppins'; " + - "-fx-text-fill: #ffffffff;" + "-fx-background-radius: 18 5 18 18;" ); } else { textBubble.setStyle( "-fx-background-color: #FFFFFF; " + - "-fx-background-radius: 5 18 18 18; " + - "-fx-font-size: 14px; " + - "-fx-font-family: 'Poppins'; " + - "-fx-text-fill: #3F3F5B;" + "-fx-background-radius: 5 18 18 18;" ); } @@ -231,6 +228,47 @@ private Label createTextBubble(String message, boolean isUser) { return textBubble; } + /** + * Parses message text and adds formatted Text nodes to the TextFlow. + * Supports **bold** markdown formatting. + * + * @param textFlow The TextFlow to add Text nodes to + * @param message The message to parse + * @param isUser True if this is a user message, false for bot message + */ + private void parseAndAddFormattedText(TextFlow textFlow, String message, boolean isUser) { + // Base text color + Color textColor = isUser ? Color.WHITE : Color.rgb(63, 63, 91); + Font baseFont = Font.font("Poppins", 14); + Font boldFont = Font.font("Poppins", FontWeight.BOLD, 14); + + String[] parts = message.split("\\*\\*"); + + for (int i = 0; i < parts.length; i++) { + if (!parts[i].isEmpty()) { + Text text = new Text(parts[i]); + text.setFill(textColor); + + // Apply bold formatting to odd-indexed parts (text between **) + if (i % 2 == 1) { + text.setFont(boldFont); + } else { + text.setFont(baseFont); + } + + textFlow.getChildren().add(text); + } + } + + // If no text was added (empty message), add a space + if (textFlow.getChildren().isEmpty()) { + Text text = new Text(" "); + text.setFill(textColor); + text.setFont(baseFont); + textFlow.getChildren().add(text); + } + } + /** * Gets the bot avatar path. * @@ -238,7 +276,7 @@ private Label createTextBubble(String message, boolean isUser) { */ private String getBotAvatarPath() { // Return the same format as used in UserData.xml - return "src/main/resources/com/starlight/images/dummy/profilewoman.jpg"; + return "src/main/resources/com/starlight/images/botIcon.png"; } /** @@ -260,8 +298,7 @@ private String getUserAvatarPath() { @FXML private void handleAttachImage() { // TODO: Implement image attachment functionality - attachmentStatus.set("Image attachment not yet implemented"); - attachmentnama.setText(attachmentStatus.get()); + attachmentnama.setText("Image attachment not yet implemented"); } /** @@ -278,21 +315,6 @@ private void handlePromptKeyPress(KeyEvent event) { @Override public void initialize(URL url, ResourceBundle rb) { - // Initialize FXML bindings - setupBindings(); - - // Make the TextArea transparent - prompt.setStyle("-fx-background-color: transparent; -fx-border-color: transparent;"); - - // Add welcome message - addBubble("Hello! I'm Fumo, your nutrition assistant. How can I help you today?", false); - } - - /** - * Sets up property bindings for reactive UI updates. - * This replaces manual event handler setup with declarative bindings. - */ - private void setupBindings() { // Bind hasTextInput property to prompt text property hasTextInput.bind(prompt.textProperty().isNotEmpty()); @@ -302,24 +324,32 @@ private void setupBindings() { // Bind attachment button disable property to processing state attachImage.disableProperty().bind(isProcessing); - // Bind attachment label text to attachment status property - attachmentnama.textProperty().bind(attachmentStatus); + // Set up event handlers + send.setOnAction(e -> handleUserMessage()); + attachImage.setOnAction(e -> handleAttachImage()); + prompt.setOnKeyPressed(this::handlePromptKeyPress); - // Note: For FXML binding, you would typically use these in the FXML file: - // - // - //