Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -41,6 +50,7 @@ task coverage(type: JacocoReport) {
}

dependencies {
String testFxVersion = '4.0.16-alpha'
String jUnitVersion = '5.4.0'
String javaFxVersion = '11'

Expand All @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/seedu/address/ui/UiManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,8 @@ private void showFatalErrorDialogAndShutdown(String title, Throwable e) {
System.exit(1);
}

public static String getAlertDialogPaneFieldId() {
return ALERT_DIALOG_PANE_FIELD_ID;
}

}
119 changes: 119 additions & 0 deletions src/test/java/guitests/GuiRobot.java
Original file line number Diff line number Diff line change
@@ -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<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 {
}
}
35 changes: 35 additions & 0 deletions src/test/java/guitests/guihandles/AlertDialogHandle.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
42 changes: 42 additions & 0 deletions src/test/java/guitests/guihandles/CommandBoxHandle.java
Original file line number Diff line number Diff line change
@@ -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<TextField> {

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<String> getStyleClass() {
return getRootNode().getStyleClass();
}
}
83 changes: 83 additions & 0 deletions src/test/java/guitests/guihandles/DoctorCardHandle.java
Original file line number Diff line number Diff line change
@@ -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<Node> {
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<Label> tagLabels;

/**
* Constructs a {@code DoctorCardHandle} with the given {@code cardNode}.
* @param cardNode
*/
public DoctorCardHandle(Node cardNode) {
super(cardNode);

idLabel = getChildNode(ID_FIELD_ID);
nameLabel = getChildNode(NAME_FIELD_ID);
phoneLabel = getChildNode(PHONE_FIELD_ID);
emailLabel = getChildNode(EMAIL_FIELD_ID);

Region tagsContainer = getChildNode(TAGS_FIELD_ID);
tagLabels = tagsContainer
.getChildrenUnmodifiable()
.stream()
.map(Label.class::cast)
.collect(Collectors.toList());
}

public String getId() {
return idLabel.getText();
}

public String getName() {
return nameLabel.getText();
}

public String getPhone() {
return phoneLabel.getText();
}

public String getEmail() {
return emailLabel.getText();
}

public List<String> getTags() {
return tagLabels
.stream()
.map(Label::getText)
.collect(Collectors.toList());
}

/**
* Returns true if this handle contains {@code doctor}.
*/
public boolean equals(Doctor doctor) {
return getName().equals(doctor.getName().getValue())
&& getPhone().equals(doctor.getPhone().getValue())
&& getEmail().equals(doctor.getEmail().getValue())
&& getTags().equals(doctor.getTags().stream()
.map(Tag::getTagName)
.sorted()
.collect(Collectors.toList()));
}
}
Loading