From 567fcb0c0f112bbbed0723dffa64f6d0af6ef582 Mon Sep 17 00:00:00 2001 From: weijian Date: Mon, 21 Oct 2024 20:45:29 +0800 Subject: [PATCH 1/7] Add CommandHistory class to keep track of past commands --- .../tuteez/logic/commands/CommandHistory.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/main/java/tuteez/logic/commands/CommandHistory.java diff --git a/src/main/java/tuteez/logic/commands/CommandHistory.java b/src/main/java/tuteez/logic/commands/CommandHistory.java new file mode 100644 index 00000000000..455ba7192a4 --- /dev/null +++ b/src/main/java/tuteez/logic/commands/CommandHistory.java @@ -0,0 +1,58 @@ +package tuteez.logic.commands; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the history of commands entered by the user. + */ +public class CommandHistory { + private final List history; + private int currentIndex; + + /** + * Creates a CommandHistory class to remember the history of commands the user entered. + * The current index is initially set to -1 to indicate that no commands have been accessed + * or selected from the history yet. + */ + public CommandHistory() { + this.history = new ArrayList<>(); + this.currentIndex = -1; + } + + /** + * Adds a new command to the history and resets the current index. + */ + public void add(String command) { + assert command != null; + history.add(command); + // Reset index to the end of the list. + currentIndex = history.size(); + } + + /** + * Returns the previous command in history, or null if no previous command exists. + */ + public String getPreviousCommand() { + assert currentIndex >= -1 && currentIndex <= history.size(); + if (currentIndex > 0) { + currentIndex--; + return history.get(currentIndex); + } + return null; // No previous command available + } + + /** + * Returns the next command in history, or null if no next command exists. + */ + public String getNextCommand() { + assert currentIndex >= -1 && currentIndex <= history.size(); + if (currentIndex < history.size() - 1) { + currentIndex++; + return history.get(currentIndex); + } + currentIndex = history.size(); + return ""; + } + +} From 67e865d1f6405e610b4e582036fffefdaa1753ae Mon Sep 17 00:00:00 2001 From: weijian Date: Mon, 21 Oct 2024 20:46:34 +0800 Subject: [PATCH 2/7] Edit the commandBox file to use the commandHistory --- src/main/java/tuteez/ui/CommandBox.java | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/main/java/tuteez/ui/CommandBox.java b/src/main/java/tuteez/ui/CommandBox.java index af385b6b090..4c3cc3a0096 100644 --- a/src/main/java/tuteez/ui/CommandBox.java +++ b/src/main/java/tuteez/ui/CommandBox.java @@ -3,7 +3,10 @@ import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; +import tuteez.logic.commands.CommandHistory; import tuteez.logic.commands.CommandResult; import tuteez.logic.commands.exceptions.CommandException; import tuteez.logic.parser.exceptions.ParseException; @@ -18,6 +21,8 @@ public class CommandBox extends UiPart { private final CommandExecutor commandExecutor; + private final CommandHistory commandHistory; + @FXML private TextField commandTextField; @@ -27,8 +32,11 @@ public class CommandBox extends UiPart { public CommandBox(CommandExecutor commandExecutor) { super(FXML); this.commandExecutor = commandExecutor; + this.commandHistory = new CommandHistory(); // calls #setStyleToDefault() whenever there is a change to the text of the command box. commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); + + commandTextField.addEventFilter(KeyEvent.KEY_PRESSED, this::handleKeyPressed); } /** @@ -43,12 +51,40 @@ private void handleCommandEntered() { try { commandExecutor.execute(commandText); + commandHistory.add(commandText); commandTextField.setText(""); } catch (CommandException | ParseException e) { setStyleToIndicateCommandFailure(); } } + /** + * Handles key press events for navigating the command history. + */ + private void handleKeyPressed(KeyEvent event) { + if (event.getCode() == KeyCode.UP || event.getCode() == KeyCode.DOWN) { + String command = (event.getCode() == KeyCode.UP) + ? commandHistory.getPreviousCommand() + : commandHistory.getNextCommand(); + + if (command != null) { + updateCommandField(command); + } + + event.consume(); + } + } + + /** + * Updates the command text field and sets the caret position to the end. + * + * @param command The command to set in the text field. + */ + private void updateCommandField(String command) { + commandTextField.setText(command); + commandTextField.positionCaret(command.length()); + } + /** * Sets the command box style to use the default style. */ From 41a7cb2d8a9f0ecc5348ac190077c703e7e22432 Mon Sep 17 00:00:00 2001 From: weijian Date: Mon, 21 Oct 2024 20:51:16 +0800 Subject: [PATCH 3/7] Fix a bug where clicking on menu bar interferes with arrow keys There is a bug where if the user clicks on the menu bar, the focus will be transferred to the menu bar component. This interferes with the up and down arrow keys used for command history. Let's change the UI so that when users click on the menu bar, the commandBox will be unfocused so that users must click on the command box again to focus on it. --- src/main/java/tuteez/ui/MainWindow.java | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/tuteez/ui/MainWindow.java b/src/main/java/tuteez/ui/MainWindow.java index 5151d9fadf0..688a88d7196 100644 --- a/src/main/java/tuteez/ui/MainWindow.java +++ b/src/main/java/tuteez/ui/MainWindow.java @@ -4,10 +4,12 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import tuteez.commons.core.GuiSettings; @@ -38,6 +40,9 @@ public class MainWindow extends UiPart { @FXML private StackPane commandBoxPlaceholder; + @FXML + private MenuBar menuBar; + @FXML private MenuItem helpMenuItem; @@ -66,6 +71,8 @@ public MainWindow(Stage primaryStage, Logic logic) { setAccelerators(); helpWindow = new HelpWindow(); + + setupMenuBarListener(); } public Stage getPrimaryStage() { @@ -135,6 +142,22 @@ private void setWindowDefaultSize(GuiSettings guiSettings) { } } + /** + * Sets up the event listeners for the menu bar. + */ + private void setupMenuBarListener() { + menuBar.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> handleMenuBarClick()); + } + + /** + * Handles the click event for the menu bar to transfer focus away from command box to resultDisplay. + * This method is necessary to ensure that when the up and down arrow keys are recorded, + * the menu bar does not interfere with the command box's input. + */ + private void handleMenuBarClick() { + resultDisplayPlaceholder.requestFocus(); + } + /** * Opens the help window or focuses on it if it's already opened. */ From d80f250d8c1388e59158d952201a641847e6202e Mon Sep 17 00:00:00 2001 From: weijian Date: Mon, 21 Oct 2024 20:51:32 +0800 Subject: [PATCH 4/7] Add unit test for CommandHistory --- .../logic/commands/CommandHistoryTest.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/test/java/tuteez/logic/commands/CommandHistoryTest.java diff --git a/src/test/java/tuteez/logic/commands/CommandHistoryTest.java b/src/test/java/tuteez/logic/commands/CommandHistoryTest.java new file mode 100644 index 00000000000..c1beacc585d --- /dev/null +++ b/src/test/java/tuteez/logic/commands/CommandHistoryTest.java @@ -0,0 +1,59 @@ +package tuteez.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class CommandHistoryTest { + private CommandHistory commandHistory; + + @BeforeEach + public void setUp() { + commandHistory = new CommandHistory(); + } + + @Test + public void add_nullCommand_throwsException() { + // Check that adding a null command throws an exception + assertThrows(AssertionError.class, () -> commandHistory.add(null)); + } + + @Test + public void add_correctlyAddsCommands() { + commandHistory.add("command1"); + commandHistory.add("command2"); + + // Check if the commands have been added correctly + assertEquals("command2", commandHistory.getPreviousCommand()); + assertEquals("command1", commandHistory.getPreviousCommand()); + + // Ensure currentIndex is reset to the end of the list after adding commands + commandHistory.add("command3"); + assertEquals("command3", commandHistory.getPreviousCommand()); // Should return command3 + } + @Test + public void getPreviousCommand_correctlyGetsPreviousCommands() { + commandHistory.add("command1"); + commandHistory.add("command2"); + commandHistory.add("command3"); + + // Check if the last command can be retrieved + assertEquals("command3", commandHistory.getPreviousCommand()); + assertEquals("command2", commandHistory.getPreviousCommand()); + assertEquals("command1", commandHistory.getPreviousCommand()); + assertNull(commandHistory.getPreviousCommand()); + } + + @Test + public void getNextCommand_correctlyGetsNextCommands() { + commandHistory.add("command1"); + commandHistory.add("command2"); + assertEquals("command2", commandHistory.getPreviousCommand()); + assertEquals("command1", commandHistory.getPreviousCommand()); + assertEquals("command2", commandHistory.getNextCommand()); + assertEquals("", commandHistory.getNextCommand()); // No more next commands + } +} From 1031405826a8a3e9bc20ccc9f5d3f16db87da09e Mon Sep 17 00:00:00 2001 From: weijian Date: Tue, 22 Oct 2024 00:42:00 +0800 Subject: [PATCH 5/7] Create automated GUI testing for commandBox --- build.gradle | 9 +++ src/test/java/tuteez/ui/CommandBoxTest.java | 77 +++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/test/java/tuteez/ui/CommandBoxTest.java diff --git a/build.gradle b/build.gradle index d3713150cb8..38e664c9064 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,12 @@ checkstyle { test { useJUnitPlatform() finalizedBy jacocoTestReport + // Add JVM arguments to open the necessary JavaFX modules for TestFX + jvmArgs += [ + '--add-opens', 'javafx.graphics/com.sun.javafx.application=ALL-UNNAMED', + '--add-opens', 'javafx.base/com.sun.javafx.runtime=ALL-UNNAMED', + '--add-opens', 'javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED' + ] } task coverage(type: JacocoReport) { @@ -60,6 +66,9 @@ dependencies { implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' + testImplementation 'org.testfx:testfx-core:4.0.16-alpha' + testImplementation 'org.testfx:testfx-junit5:4.0.16-alpha' + testImplementation 'org.testfx:openjfx-monocle:jdk-11+26' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion diff --git a/src/test/java/tuteez/ui/CommandBoxTest.java b/src/test/java/tuteez/ui/CommandBoxTest.java new file mode 100644 index 00000000000..6dff27c7524 --- /dev/null +++ b/src/test/java/tuteez/ui/CommandBoxTest.java @@ -0,0 +1,77 @@ +package tuteez.ui; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testfx.api.FxRobot; +import org.testfx.framework.junit5.ApplicationExtension; +import org.testfx.framework.junit5.Start; + +import javafx.scene.Scene; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.stage.Stage; +import tuteez.logic.commands.CommandResult; + +@ExtendWith(ApplicationExtension.class) +public class CommandBoxTest { + private CommandBox commandBox; + private TextField commandTextField; + + @Start + public void start(Stage stage) { + // Set up your GUI for testing + commandBox = new CommandBox(commandText -> new CommandResult("Command executed")); + stage.setScene(new Scene(commandBox.getRoot())); + stage.show(); + } + + @Test + public void handleCommandEntered_validCommand(FxRobot robot) { + // Simulate typing in the text field + commandTextField = robot.lookup("#commandTextField").queryAs(TextField.class); + robot.clickOn(commandTextField).write("list"); + + // Press Enter to simulate command submission + robot.type(KeyCode.ENTER); + + // Assert that the text field is cleared + assertEquals("", commandTextField.getText()); + } + + @Test + public void handleKeyPressed_upArrowRetrievesPreviousCommand(FxRobot robot) { + // Add commands to history manually for this test + commandTextField = robot.lookup("#commandTextField").queryAs(TextField.class); + robot.clickOn(commandTextField).write("list"); + robot.type(KeyCode.ENTER); + + commandTextField = robot.lookup("#commandTextField").queryAs(TextField.class); + robot.clickOn(commandTextField).write("help"); + robot.type(KeyCode.ENTER); + // Simulate UP key press + robot.type(KeyCode.UP); + + // Assert that the text field shows the previous command + assertEquals("help", commandTextField.getText()); + } + + @Test + public void handleKeyPressed_downArrowClearsCommand(FxRobot robot) { + // Add a command to history + commandTextField = robot.lookup("#commandTextField").queryAs(TextField.class); + robot.clickOn(commandTextField).write("list"); + robot.type(KeyCode.ENTER); + + // Navigate back to the first command + robot.type(KeyCode.UP); + assertEquals("list", commandTextField.getText()); + + // Press DOWN to clear the command + robot.type(KeyCode.DOWN); + + // Assert that the text field is cleared + assertEquals("", commandTextField.getText()); + } +} From 93d99a1e8ab3bac7d3ed456ebe349d822a0a7ebd Mon Sep 17 00:00:00 2001 From: weijian Date: Tue, 22 Oct 2024 00:42:46 +0800 Subject: [PATCH 6/7] Add test cases for assertions in CommmandHistory --- .../tuteez/logic/commands/CommandHistory.java | 2 +- .../logic/commands/CommandHistoryTest.java | 32 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/java/tuteez/logic/commands/CommandHistory.java b/src/main/java/tuteez/logic/commands/CommandHistory.java index 455ba7192a4..59ab4f66362 100644 --- a/src/main/java/tuteez/logic/commands/CommandHistory.java +++ b/src/main/java/tuteez/logic/commands/CommandHistory.java @@ -43,7 +43,7 @@ public String getPreviousCommand() { } /** - * Returns the next command in history, or null if no next command exists. + * Returns the next command in history, or an empty string if no next command exists. */ public String getNextCommand() { assert currentIndex >= -1 && currentIndex <= history.size(); diff --git a/src/test/java/tuteez/logic/commands/CommandHistoryTest.java b/src/test/java/tuteez/logic/commands/CommandHistoryTest.java index c1beacc585d..ddf5f8fdb80 100644 --- a/src/test/java/tuteez/logic/commands/CommandHistoryTest.java +++ b/src/test/java/tuteez/logic/commands/CommandHistoryTest.java @@ -4,9 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.lang.reflect.Field; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - public class CommandHistoryTest { private CommandHistory commandHistory; @@ -56,4 +57,33 @@ public void getNextCommand_correctlyGetsNextCommands() { assertEquals("command2", commandHistory.getNextCommand()); assertEquals("", commandHistory.getNextCommand()); // No more next commands } + + @Test + public void getNextCommand_invalidCurrentIndex_throwsAssertionError() throws Exception { + commandHistory.add("command1"); + commandHistory.add("command2"); + + // Use reflection to set currentIndex to an invalid value (e.g., larger than history.size()) + Field field = CommandHistory.class.getDeclaredField("currentIndex"); + field.setAccessible(true); + field.set(commandHistory, 999); // Invalid index + + // Expect an AssertionError when calling getNextCommand() + assertThrows(AssertionError.class, () -> commandHistory.getNextCommand()); + } + + @Test + public void getPreviousCommand_invalidCurrentIndex_throwsAssertionError() throws Exception { + commandHistory.add("command1"); + commandHistory.add("command2"); + + // Use reflection to set currentIndex to an invalid value (e.g., negative beyond -1) + Field field = CommandHistory.class.getDeclaredField("currentIndex"); + field.setAccessible(true); + field.set(commandHistory, -999); // Invalid index + + // Expect an AssertionError when calling getPreviousCommand() + assertThrows(AssertionError.class, () -> commandHistory.getPreviousCommand()); + } + } From 58b71372e891bf2677efa641628ad1127a686752 Mon Sep 17 00:00:00 2001 From: weijian Date: Tue, 22 Oct 2024 09:13:38 +0800 Subject: [PATCH 7/7] Remove commandBox UI test and dependencies --- build.gradle | 9 --- src/test/java/tuteez/ui/CommandBoxTest.java | 77 --------------------- 2 files changed, 86 deletions(-) delete mode 100644 src/test/java/tuteez/ui/CommandBoxTest.java diff --git a/build.gradle b/build.gradle index 38e664c9064..d3713150cb8 100644 --- a/build.gradle +++ b/build.gradle @@ -23,12 +23,6 @@ checkstyle { test { useJUnitPlatform() finalizedBy jacocoTestReport - // Add JVM arguments to open the necessary JavaFX modules for TestFX - jvmArgs += [ - '--add-opens', 'javafx.graphics/com.sun.javafx.application=ALL-UNNAMED', - '--add-opens', 'javafx.base/com.sun.javafx.runtime=ALL-UNNAMED', - '--add-opens', 'javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED' - ] } task coverage(type: JacocoReport) { @@ -66,9 +60,6 @@ dependencies { implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' - testImplementation 'org.testfx:testfx-core:4.0.16-alpha' - testImplementation 'org.testfx:testfx-junit5:4.0.16-alpha' - testImplementation 'org.testfx:openjfx-monocle:jdk-11+26' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion diff --git a/src/test/java/tuteez/ui/CommandBoxTest.java b/src/test/java/tuteez/ui/CommandBoxTest.java deleted file mode 100644 index 6dff27c7524..00000000000 --- a/src/test/java/tuteez/ui/CommandBoxTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package tuteez.ui; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.testfx.api.FxRobot; -import org.testfx.framework.junit5.ApplicationExtension; -import org.testfx.framework.junit5.Start; - -import javafx.scene.Scene; -import javafx.scene.control.TextField; -import javafx.scene.input.KeyCode; -import javafx.stage.Stage; -import tuteez.logic.commands.CommandResult; - -@ExtendWith(ApplicationExtension.class) -public class CommandBoxTest { - private CommandBox commandBox; - private TextField commandTextField; - - @Start - public void start(Stage stage) { - // Set up your GUI for testing - commandBox = new CommandBox(commandText -> new CommandResult("Command executed")); - stage.setScene(new Scene(commandBox.getRoot())); - stage.show(); - } - - @Test - public void handleCommandEntered_validCommand(FxRobot robot) { - // Simulate typing in the text field - commandTextField = robot.lookup("#commandTextField").queryAs(TextField.class); - robot.clickOn(commandTextField).write("list"); - - // Press Enter to simulate command submission - robot.type(KeyCode.ENTER); - - // Assert that the text field is cleared - assertEquals("", commandTextField.getText()); - } - - @Test - public void handleKeyPressed_upArrowRetrievesPreviousCommand(FxRobot robot) { - // Add commands to history manually for this test - commandTextField = robot.lookup("#commandTextField").queryAs(TextField.class); - robot.clickOn(commandTextField).write("list"); - robot.type(KeyCode.ENTER); - - commandTextField = robot.lookup("#commandTextField").queryAs(TextField.class); - robot.clickOn(commandTextField).write("help"); - robot.type(KeyCode.ENTER); - // Simulate UP key press - robot.type(KeyCode.UP); - - // Assert that the text field shows the previous command - assertEquals("help", commandTextField.getText()); - } - - @Test - public void handleKeyPressed_downArrowClearsCommand(FxRobot robot) { - // Add a command to history - commandTextField = robot.lookup("#commandTextField").queryAs(TextField.class); - robot.clickOn(commandTextField).write("list"); - robot.type(KeyCode.ENTER); - - // Navigate back to the first command - robot.type(KeyCode.UP); - assertEquals("list", commandTextField.getText()); - - // Press DOWN to clear the command - robot.type(KeyCode.DOWN); - - // Assert that the text field is cleared - assertEquals("", commandTextField.getText()); - } -}