diff --git a/build.gradle b/build.gradle index 31a3ff9df0d..928bdb61ba8 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,15 @@ checkstyle { test { useJUnitPlatform() finalizedBy jacocoTestReport + + //Solution adapted from https://github.com/nus-cs2103-AY2223S2/forum/issues/350 + jvmArgs "-Dheadless=${project.hasProperty('headless') ? project.headless : false}" + systemProperties = [ + 'testfx.robot': 'glass', + 'testfx.headless': 'true', + 'prism.order': 'sw', + 'prism.text': 't2k', + ] } task coverage(type: JacocoReport) { @@ -41,6 +50,7 @@ task coverage(type: JacocoReport) { } dependencies { + String testFxVersion = '4.0.16-alpha' String jUnitVersion = '5.4.0' String javaFxVersion = '11' @@ -60,7 +70,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 group: 'org.testfx', name: 'testfx-core', version: testFxVersion testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion + testImplementation group: 'org.testfx', name: 'openjfx-monocle', version: 'jdk-12.0.1+2' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index 515b6fb234b..a612ca1b591 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -85,4 +85,8 @@ private void showFatalErrorDialogAndShutdown(String title, Throwable e) { System.exit(1); } + public static String getAlertDialogPaneFieldId() { + return ALERT_DIALOG_PANE_FIELD_ID; + } + } diff --git a/src/test/java/guitests/GuiRobot.java b/src/test/java/guitests/GuiRobot.java new file mode 100644 index 00000000000..269f1b2fc0d --- /dev/null +++ b/src/test/java/guitests/GuiRobot.java @@ -0,0 +1,119 @@ +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; + + /** + * Creates a new GuiRobot. + */ + 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. + * + * @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 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 { + } +} diff --git a/src/test/java/guitests/guihandles/AlertDialogHandle.java b/src/test/java/guitests/guihandles/AlertDialogHandle.java new file mode 100644 index 00000000000..e3957e76431 --- /dev/null +++ b/src/test/java/guitests/guihandles/AlertDialogHandle.java @@ -0,0 +1,35 @@ +package guitests.guihandles; + +import javafx.scene.control.DialogPane; +import javafx.stage.Stage; +import seedu.address.ui.UiManager; + +/** + * A handle for the {@code AlertDialog} of the UI. + */ +public class AlertDialogHandle extends StageHandle { + private final DialogPane dialogPane; + + /** + * Constructs a {@code AlertDialogHandle} with the given {@code stage}. + */ + public AlertDialogHandle(Stage stage) { + super(stage); + + dialogPane = getChildNode("#" + UiManager.getAlertDialogPaneFieldId()); + } + + /** + * Returns the text of the header in the {@code AlertDialog}. + */ + public String getHeaderText() { + return dialogPane.getHeaderText(); + } + + /** + * Returns the text of the content in the {@code AlertDialog}. + */ + public String getContentText() { + return dialogPane.getContentText(); + } +} diff --git a/src/test/java/guitests/guihandles/CommandBoxHandle.java b/src/test/java/guitests/guihandles/CommandBoxHandle.java new file mode 100644 index 00000000000..aa59b1e470e --- /dev/null +++ b/src/test/java/guitests/guihandles/CommandBoxHandle.java @@ -0,0 +1,42 @@ +package guitests.guihandles; + +import javafx.collections.ObservableList; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; + +/** + * A handle to the {@code CommandBox} in the GUI. + */ +public class CommandBoxHandle extends NodeHandle { + + public static final String COMMAND_INPUT_FIELD_ID = "#commandTextField"; + + public CommandBoxHandle(TextField commandBoxNode) { + super(commandBoxNode); + } + + /** + * Returns the text in the command box. + */ + public String getInput() { + return getRootNode().getText(); + } + + /** + * Enters the given command in the Command Box and presses enter. + */ + public void run(String command) { + click(); + guiRobot.interact(() -> getRootNode().setText(command)); + guiRobot.pauseForHuman(); + + guiRobot.type(KeyCode.ENTER); + } + + /** + * Returns the list of style classes present in the command box. + */ + public ObservableList getStyleClass() { + return getRootNode().getStyleClass(); + } +} diff --git a/src/test/java/guitests/guihandles/DoctorCardHandle.java b/src/test/java/guitests/guihandles/DoctorCardHandle.java new file mode 100644 index 00000000000..5391245df38 --- /dev/null +++ b/src/test/java/guitests/guihandles/DoctorCardHandle.java @@ -0,0 +1,83 @@ +package guitests.guihandles; + +import java.util.List; +import java.util.stream.Collectors; + +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import seedu.address.model.person.doctor.Doctor; +import seedu.address.model.tag.Tag; + +/** + * Provides a handle to a doctor card in the doctor list panel. + */ +public class DoctorCardHandle extends NodeHandle { + private static final String ID_FIELD_ID = "#id"; + private static final String NAME_FIELD_ID = "#name"; + private static final String PHONE_FIELD_ID = "#phone"; + private static final String EMAIL_FIELD_ID = "#email"; + private static final String TAGS_FIELD_ID = "#tags"; + + private final Label idLabel; + private final Label nameLabel; + private final Label phoneLabel; + private final Label emailLabel; + private final List