Skip to content

Commit

Permalink
Merge pull request AY2425S1-CS2103T-T12-4#210
Browse files Browse the repository at this point in the history
Add Automated GUI testing and minor bug Fixes
  • Loading branch information
JJtan2002 authored Nov 3, 2024
2 parents b1d3f7c + 1c7e279 commit 1c635eb
Show file tree
Hide file tree
Showing 37 changed files with 1,639 additions and 28 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ jobs:
java-package: jdk+fx

- name: Build and check with Gradle
if: runner.os != 'Linux'
run: ./gradlew check coverage

- name: Upload coverage reports to Codecov
if: runner.os == 'Linux'
if: runner.os == 'Windows'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ task coverage(type: JacocoReport) {
}

dependencies {
String testFxVersion = '4.0.15-alpha'
String jUnitVersion = '5.4.0'
String javaFxVersion = '17.0.7'

Expand All @@ -64,7 +65,9 @@ dependencies {
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4'

testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion
testImplementation group: 'org.testfx', name: 'testfx-core', version: testFxVersion

testRuntimeOnly group: 'org.testfx', name: 'openjfx-monocle', version: 'jdk-11+26'
testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion
}

Expand Down
3 changes: 3 additions & 0 deletions docs/DeveloperGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ This project is an extension of [_AddressBook-Level3 by se-edu_](https://se-educ

Libraries used include: [_JavaFX_](https://openjfx.io/), [_Jackson_](https://github.com/FasterXML/jackson), [_JUnit5_](https://github.com/junit-team/junit5)

Graphical Interface (GUI) Testing was adapted from [_AddressBook-Level4 by se-edu_](https://se-education.org/addressbook-level4/)


--------------------------------------------------------------------------------------------------------------------

## **Setting up, getting started**
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/seedu/address/storage/JsonAddressBookStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ public JsonAddressBookStorage(Path filePath, Path manualSaveFilePath) {
this.manualSaveFilePath = manualSaveFilePath;
}

/**
* Creates a new JsonAddressBookStorage object.
*
* @param filePath The path to the JSON file where the address book data will be stored.
* Constructor if only 1 path is specified will assume manual and default are the same path
*
*/
public JsonAddressBookStorage(Path filePath) {
this.filePath = filePath;
this.manualSaveFilePath = filePath;
}

public Path getAddressBookFilePath() {
return filePath;
}
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/seedu/address/ui/CommandBox.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,23 @@ public CommandBox(CommandExecutor commandExecutor) {
@FXML
public void initialize() {
commandTextField.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.UP) {
if (event.getCode() == KeyCode.UP && !event.isShiftDown()) {
String previousCommand = commandHistory.getPreviousCommand();
if (previousCommand != null) {
commandTextField.setText(previousCommand);
commandTextField.positionCaret(previousCommand.length()); // Move cursor to end
}
} else if (event.getCode() == KeyCode.DOWN) {
}
if (event.getCode() == KeyCode.DOWN && !event.isShiftDown()) {
String nextCommand = commandHistory.getNextCommand();
if (nextCommand != null) {
commandTextField.setText(nextCommand);
commandTextField.positionCaret(nextCommand.length()); // Move cursor to end
}
}
if (event.getCode() == KeyCode.ENTER) {
handleCommandEntered();
}
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/seedu/address/ui/CommandHistory.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ public String getNextCommand() {
currentHistoryIndex++;
return commandHistory.get(currentHistoryIndex);
}
return null; // No next command
return ""; // No next command
}
}
69 changes: 54 additions & 15 deletions src/main/java/seedu/address/ui/PersonCard.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@
import java.util.Comparator;

import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import seedu.address.model.game.Game;
import seedu.address.model.game.Role;
import seedu.address.model.game.SkillLevel;
Expand Down Expand Up @@ -67,35 +74,67 @@ public PersonCard(Person person, int displayedIndex) {
.forEach(tag -> tags.getChildren().add(new Label(tag.tagName)));
person.getGames().values().stream()
.sorted(Comparator.comparing(game -> game.gameName))
.forEach(game -> games.getChildren().add(gameLabel(game)));
.forEach(game -> games.getChildren().add(gameTextFlow(game)));
person.getPreferredTimes().stream()
.sorted(Comparator.comparing(preferredTime -> preferredTime.preferredTime))
.forEach(preferredTime -> preferredTimes.getChildren().add(new Label(preferredTime.preferredTime)));
.forEach(preferredTime -> preferredTimes.getChildren().add(new Label("Preferred time: "
+ preferredTime.preferredTime)));
}

private static Label gameLabel(Game game) {
private static Label gameTextFlow(Game game) {
// Create the main label that will hold everything
Label label = new Label();

// Image for favorite status
ImageView imageView = null;
if (game.getFavouriteStatus()) {
Image image = new Image(String.valueOf(PersonCard.class.getResource("/images/star.png")));
imageView = new ImageView(image);
imageView.setFitHeight(20); // Adjust size as needed
imageView.setPreserveRatio(true);
}

// Create styled "Game: gamename" part with a larger font
Text gameLabel = new Text("Game: " + game.getGameName() + "\n");
gameLabel.setFont(Font.font("Comfortaa", FontWeight.BOLD, 20)); // Larger font for game name
gameLabel.setFill(Color.WHITE);

// Build details text
StringBuilder sb = new StringBuilder();
Username username = game.getUsername();
SkillLevel skillLevel = game.getSkillLevel();
Role role = game.getRole();
boolean isFavourite = game.getFavouriteStatus();
sb.append(game.getGameName()).append("\n");
if (!username.getUsername().toString().isEmpty()) {
sb.append("Username: ").append(game.getUsername()).append("\n");
sb.append("Username: ").append(username);
}
if (!skillLevel.toString().isEmpty()) {
sb.append("Skill Lvl: ").append(game.getSkillLevel()).append("\n");
sb.append("\n").append("Skill Lvl: ").append(skillLevel);
}
if (!role.toString().isEmpty()) {
sb.append("Role: ").append(game.getRole()).append("\n");
sb.append("\n").append("Role: ").append(role);
}
if (isFavourite) {
Image image = new Image(String.valueOf(PersonCard.class.getResource("/images/star.png")));
ImageView iw = new ImageView(image);
return new Label(sb.toString(), iw);

// Add the details text
Text detailsText = new Text(sb.toString());
detailsText.setFont(Font.font("System", FontWeight.NORMAL, 14)); // Regular font for other details
detailsText.setFill(Color.WHITE);

// Create a TextFlow and add both game name and details texts
TextFlow textFlow = new TextFlow(gameLabel, detailsText);

// Set up an HBox to hold the ImageView and TextFlow side-by-side
HBox hbox = new HBox(5); // Add spacing between image and text
hbox.setAlignment(Pos.CENTER);
hbox.setPadding(new Insets(5));
if (imageView != null) {
hbox.getChildren().add(imageView); // Add star image on the left if favorite
}
assert !sb.isEmpty();
return new Label(sb.toString());
}

hbox.getChildren().add(textFlow); // Add the text to the right of the image

// Set HBox as the graphic of the label
label.setGraphic(hbox);
label.setPrefHeight(textFlow.getHeight());
return label;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ public AutoSuggestionTextField() {
}
};

suggestionList.setOnMouseClicked(event -> {
TextFlow selectedTextFlow = suggestionList.getSelectionModel().getSelectedItem();
if (selectedTextFlow instanceof CommandTextFlow) {
CommandTextFlow commandFlow = (CommandTextFlow) selectedTextFlow;
setText(commandFlow.getCommandText());
this.positionCaret(commandFlow.getCommandText().length());
suggestionPopup.hide();
}
});

// Add the handler to both the TextField and ListView
this.addEventHandler(KeyEvent.KEY_PRESSED, keyEventHandler);
suggestionList.addEventHandler(KeyEvent.KEY_PRESSED, keyEventHandler);
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/view/CommandBox.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<?import seedu.address.ui.commandpopup.AutoSuggestionTextField?>

<StackPane styleClass="stack-pane" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1">
<AutoSuggestionTextField fx:id="commandTextField" onAction="#handleCommandEntered" promptText="Enter command here... enter 'help' if you're stuck!"/>
<AutoSuggestionTextField fx:id="commandTextField" onAction="#handleCommandEntered" promptText="Enter command here... enter 'help' or press 'F1' if you're stuck!"/>
</StackPane>

6 changes: 3 additions & 3 deletions src/main/resources/view/DarkTheme.css
Original file line number Diff line number Diff line change
Expand Up @@ -356,13 +356,13 @@
-fx-vgap: 3;
}

#games .label {
#games .label {
-fx-font-family: Comfortaa;
-fx-text-fill: white;
-fx-background-color: #57a83f;
-fx-padding: 1 3 1 3;
-fx-border-radius: 2;
-fx-background-radius: 2;
-fx-border-radius: 3;
-fx-background-radius: 7;
-fx-font-size: 11;
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/view/MainWindow.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<?import javafx.scene.layout.VBox?>

<fx:root type="javafx.stage.Stage" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1"
title="Address App" minWidth="900" minHeight="600" onCloseRequest="#handleExit">
title="GamerBook PRO MAX" minWidth="900" minHeight="600" onCloseRequest="#handleExit">
<icons>
<Image url="@/images/address_book_32.png" />
</icons>
Expand Down
121 changes: 121 additions & 0 deletions src/test/java/guitests/GuiRobot.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package guitests;

import java.util.Optional;
import java.util.function.BooleanSupplier;

import org.testfx.api.FxRobot;

import guitests.guihandles.exceptions.StageNotFoundException;
import javafx.stage.Stage;

/**
* Robot used to simulate user actions on the GUI.
* Extends {@link FxRobot} by adding some customized functionality and workarounds.
*/
public class GuiRobot extends FxRobot {

private static final int PAUSE_FOR_HUMAN_DELAY_MILLISECONDS = 250;
private static final int DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MILLISECONDS = 5000;

private static final String PROPERTY_TESTFX_HEADLESS = "testfx.headless";

private final boolean isHeadlessMode;

/**
* GuiRobot constructor
*/
public GuiRobot() {
String headlessPropertyValue = System.getProperty(PROPERTY_TESTFX_HEADLESS);
isHeadlessMode = headlessPropertyValue != null && headlessPropertyValue.equals("true");
}

/**
* Pauses execution for {@code PAUSE_FOR_HUMAN_DELAY_MILLISECONDS} milliseconds for a human to examine the
* effects of the test. This method will be disabled when the GUI tests are executed in headless mode to avoid
* unnecessary delays.
*/
public void pauseForHuman() {
if (isHeadlessMode) {
return;
}

sleep(PAUSE_FOR_HUMAN_DELAY_MILLISECONDS);
}

/**
* Returns true if tests are run in headless mode.
*/
public boolean isHeadlessMode() {
return isHeadlessMode;
}

/**
* Waits for {@code event} to be true by {@code DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MILLISECONDS} milliseconds.mdi
* @throws EventTimeoutException if the time taken exceeds {@code DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MILLISECONDS}
* milliseconds.
*/
public void waitForEvent(BooleanSupplier event) {
waitForEvent(event, DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MILLISECONDS);
}

/**
* Waits for {@code event} to be true.
*
* @param timeOut in milliseconds
* @throws EventTimeoutException if the time taken exceeds {@code timeOut}.
*/
public void waitForEvent(BooleanSupplier event, int timeOut) {
int timePassed = 0;
final int retryInterval = 50;

while (!event.getAsBoolean()) {
sleep(retryInterval);
timePassed += retryInterval;

if (timePassed >= timeOut) {
throw new EventTimeoutException();
}
}

pauseForHuman();
}

/**
* Returns true if the window with {@code stageTitle} is currently open.
*/
public boolean isWindowShown(String stageTitle) {
return getNumberOfWindowsShown(stageTitle) >= 1;
}

/**
* Returns the number of windows with {@code stageTitle} that are currently open.
*/
public int getNumberOfWindowsShown(String stageTitle) {
return (int) listTargetWindows().stream()
.filter(window -> window instanceof Stage && ((Stage) window).getTitle().equals(stageTitle))
.count();
}

/**
* Returns the first stage, ordered by proximity to the current target window, with the stage title.
* The order that the windows are searched are as follows (proximity): current target window,
* children of the target window, rest of the windows.
*
* @throws StageNotFoundException if the stage is not found.
*/
public Stage getStage(String stageTitle) {
Optional<Stage> targetStage = listTargetWindows().stream()
.filter(Stage.class::isInstance) // checks that the window is of type Stage
.map(Stage.class::cast)
.filter(stage -> stage.getTitle().equals(stageTitle))
.findFirst();

return targetStage.orElseThrow(StageNotFoundException::new);
}

/**
* Represents an error which occurs when a timeout occurs when waiting for an event.
*/
private class EventTimeoutException extends RuntimeException {
}
}
Loading

0 comments on commit 1c635eb

Please sign in to comment.