diff --git a/end-to-end-tests/.gitignore b/end-to-end-tests/.gitignore new file mode 100644 index 00000000..dc03a7c5 --- /dev/null +++ b/end-to-end-tests/.gitignore @@ -0,0 +1,3 @@ +target/* +.idea/* +.allure/* diff --git a/end-to-end-tests/Readme.md b/end-to-end-tests/Readme.md new file mode 100644 index 00000000..4dab6597 --- /dev/null +++ b/end-to-end-tests/Readme.md @@ -0,0 +1,90 @@ +# Integration Tests for PhenomeCentral + +##### End-to-end tests on a native web browser using Selenium + +Jira story link: https://phenotips.atlassian.net/browse/PC-325 + +## Major Maven Dependencies +- [Selenium WebDriver](https://www.seleniumhq.org/projects/webdriver/) +- [WebDriverManager](https://github.com/bonigarcia/webdrivermanager) +- [TestNG](https://testng.org/doc/index.html) +- [Allure](http://allure.qatools.ru/) + +## Requirements +- JDK 1.8 or above. The codebase uses some Java 8 features such as lambdas. +- phenomecentral.org (parent project) already built. Currently tested with: `1.2-SNAPSHOT using PT 1.4.4` + +## Usage +### Quick start +- Clone this repository +- Build phenomecentral.org using `mvn install` as usual +- `cd standalone/target` and then `ls`. Ensure that one `phenomecentral-standalone*.zip` is present. +- `cd ../../end-to-end-tests/scripts` and then `./runIntegrationTests.sh` to run locally with default environment variables. + +This script will extract the PC standalone zip to a subfolder in `end-to-end-tests/target/instances`, start up the PC instance, run the tests, and generate an Allure report. The script stops the instances and SMTP upon a SIGINT (Ctrl-c). +By default, it will start PC on port 8083, with stop port 8084, run the tests using Chrome, have emails listened to on port 1025 and the email inbox page on port 8085. + +### Custom Environment +`runIntegrationTests.sh` accepts several arguments to specify several (optional) variables. `./runIntegrationTests.sh --help` for details. +Example: `./runIntegrationTests.sh --browser chrome --start 8083 --stop 8084 --emailUI 8085` + +If you *already* have a running instance of PC up and *running* (whether local or elsewhere): +- Don't use the runIntegrationTests.sh +- cd into the `end-to-end-tests` folder +- Ensure there is a fake SMTP server listening. If not and it is a local instance: `java -jar scripts/fake-smtp/MockMock.jar -p 1025 -h 8085` +- Run: `mvn test -Dsurefire.suiteXmlFiles=src/test/java/org/phenotips/endtoendtests/testcases/xml/AllTests.xml -Dbrowser=DESIRED_BROWSER -DhomePageURL=PC_INSTANCE_URL -DemailUIPageURL=EMAIL_UI_URL` replace `DESIRED_BROWSER` with one of `chrome, firefox, safari, edge, or ie` and `PC_Instance_URL` with the URL of the running instance and `EMAIL_UI_URL` with the URL to access the email inbox + - Example: `mvn test -Dsurefire.suiteXmlFiles=src/test/java/org/phenotips/endtoendtests/testcases/xml/AllTests.xml -Dbrowser=chrome -DhomePageURL=localhost:8083 -DemailUIPageURL=localhost:8085` + + +## Opening the Allure Report +- Report is stored in `target/site/allure-maven-plugin/`. Open `index.html` with a browser *other than Chrome* to view the report. +- Chrome disables loading resources from different origins or protocols by default. See http://biercoff.com/opening-local-version-of-allure-report-with-chrome/ + +## Running on IE +- These instructions need to be followed before IE can be used. Notably, you must manually disable "Enhanced Protected Mode" in the IE's settings before automated test software such as Selenium is able to interact with an IE browser. See https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver#required-configuration + +## Test Assumptions +- The test suite assumes a blank PC instance. +- Certain tests check for emails. Ensure that the PC instance is setup to allow for emails to be sent. If running locally, that would mean a fakeSMTP service has started. Even if we don't run tests for emails (there are only two or three), we still need a working SMTP to approve new users + + +## Architecture +- The design of the test suite follows a PageObject model/design pattern. +- Classes in the `pageobjects` package contain selectors for a page. It also contains methods that interact with a single webpage. For instance, in the `LoginPage` class, there is a method to type in a passed username, password, and then click on the login button. These call native Selenium methods to interact with the page, such as clicking and typing. + - `BasePage` is an abstract class that contains common methods for every page such as logging out. I.e. it contains what is on the toolbar that appears on every page. All other page inherit it. +- Classes in the `testcases` define endtoend test cases. A class should contain a "theme" of tests. Most tests should be able to run individually given that the `SetupUsers` class has been run once for the PC instance. See individual classes for details. + - `BaseTest` is an abstract class that handles what happens before and after each test. Using the `@Before/After(Test/Suite)` annotations, it handles the initiation of the web browser and screenshots on failure. All other test classes inherit it. +- Classes in the `common` package contain classes or interface that are used in both test cases and pageobjects. + - Notably, we have `CommonPatientMeasurements` which defines measurements assigned to a patient. A CommonPatientMeasurements object can be constructed during a test case as something to verify against, and it can also be constructed in a page object class to define what is on the View Patient form. + - The interfaces provide enums that are used in various classes to avoid passing in long strings for a selection dropdown. +- TestNG provides a `@Test` annotation to allow for running test methods and classes directly in an IDE by providing a main function. In intelliJ, you should see an interactable green run triangle on the test method declaration line. +- `BasePage` also contains environment variables that can get updated via parameters passed in the JVM. Certain variables such as the PC instance URL can be modified directly if you want to run these tests from the IDE without supplying environment variables. + - Change the variables `HOMEPAGE_URL`, `ADMIN_USERNAME`, `ADMIN_PASS`, etc. as needed + +- XML files that define a test suite are located in `testcases/xml`. TestNG uses one of these XML files to execute a test suite during `mvn test`. The default is `NoTests.xml`. + - By default, these end-to-end tests will not run on a build of phenomecentral.org during the `mvn install` or `mvn test` lifecycle. This prevents failure of the entire build if even one end to end test fails. Instead, we have to pass a different XML file with the `-Dsurefire.suiteXmlFiles` flag and run `mvn test` explicitly. See Usage. + - If you run `mvn test` or `mvn install` within the `end-to-end-tests` directory, notice how no Selenium tests are run + - The XML defines the order the tests should be run in. Instead of using `@dependsOnMethods`, `@Groups`, or `@priority` in the testcases code, we define the desired order on the XML. Each of those mentioned annotations creates a global variable that can conflict with each other and cause undefined behaviour if you are not careful. For example, don't mix `@priority` with `@dependsOnMethods` as the order of importance is not well defined in that case. +- Allure has an attached default listener with the `@step` annotation. Each step in a test can be seen as a call to a method from a pageObject class. + +Flow of execution: +- runIntegrationTests.sh script called: + - Parses command line arguments (browser, start, stop ports, etc.) supplied to it + - Extracts phenomecentral-standalone.zip found in parent's standalone/target folder (../../standalone/target), + - Starts instance and SMTP +-> script calls `mvn test -Dsurefire.suiteXmlFiles=src/test/java/org/phenotips/endtoendtests/testcases/xml/AllTests.xml -Dbrowser=SUPPLIED_BY_SCRIPT -DhomePageURL=SUPPLIED_BY_SCRIPT -DemailUIPageURL=SUPPLIED_BY_SCRIPT` +-> Selenium tests run +-> Script stops PC instance and SMTP +-> Allure report generated using another call to mvn +- Upon SIGINT, script stops the instance and SMTP before exiting. + +## Suggestions for future +- Implement the ability to make REST calls to generate users using the API. This might reduce the requirement of a fakeSMTP and allows users to be created much more easily +- JMeter load testing +- Investigate video recording, custom listener class or adapter to generate test and handle failure +- Multiple measurement entries, selectors and methods only work with the first measurement entry +- Other features that are bound to change: Entering data to Patient Form, Pedigree Editor, and Match Table + +## Limitations +- UI/UX issues can't be detected effectively. Ex. Element is present but is in 1 pt font or hard to see. +- A seperate machine or server is highly recommended for test stability. While Selenium does not explicitly capture the mouse or keyboard, the browser window might come into focus if a native OS error dialogue (such as the one we get when being prompted for unsaved changes) pops up. Use with caution. diff --git a/end-to-end-tests/phenomeCentralautomation.iml b/end-to-end-tests/phenomeCentralautomation.iml new file mode 100644 index 00000000..78b2cc53 --- /dev/null +++ b/end-to-end-tests/phenomeCentralautomation.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/end-to-end-tests/pom.xml b/end-to-end-tests/pom.xml new file mode 100644 index 00000000..459471de --- /dev/null +++ b/end-to-end-tests/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + org.phenomecentral + phenomeCentral-automation + 1.0-SNAPSHOT + + + 1.9.2 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + 3.8.0 + + + + io.qameta.allure + allure-maven + 2.10.0 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M3 + + + + -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" + + + + + + src/test/java/org/phenotips/endtoendtests/testcases/xml/NoTests.xml + + + + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + + + + + + + + org.seleniumhq.selenium + selenium-java + 3.141.59 + + + + + org.testng + testng + 6.8 + + + + + commons-io + commons-io + 2.6 + + + + + io.github.bonigarcia + webdrivermanager + 3.3.0 + + + + + io.qameta.allure + allure-testng + 2.10.0 + + + + + \ No newline at end of file diff --git a/end-to-end-tests/scripts/runIntegrationTests.sh b/end-to-end-tests/scripts/runIntegrationTests.sh new file mode 100755 index 00000000..4ab674a6 --- /dev/null +++ b/end-to-end-tests/scripts/runIntegrationTests.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env bash + +# This script does the following: +# - Extract zip file for standalone PC instance +# - Start the local PC instance. After 30 seconds, pings to check for $READY_MESSAGE. +# - Start a fake SMTP server (MockMock SMTP) +# - Run all integration tests (AllTests.xml) +# - Stop the SMTP and PC instances +# - Generate an Allure report with the test run results. + +########################### +# Variables +########################### + +# PC and SMTP UI ports. These can be changed as needed. +BROWSER="chrome" # One of: chrome, firefox, edge, ie, safari +PC_INSTANCE_PORT="8083" +PC_INSTANCE_STOP_PORT="8084" +EMAIL_UI_PORT="8085" # Port to access Fake SMTP (MockMock) email inbox UI +OUTGOING_EMAIL_PORT="1025" # PC instance sends emails to this port. Fake SMTP (MockMock) listens to this port. + +PC_INSTANCE_URL="http://localhost:$PC_INSTANCE_PORT" +EMAIL_UI_URL="http://localhost:$EMAIL_UI_PORT" + +CUR_DATE=$(date '+%Y-%m-%d_%H-%M-%S') # Set once +ZIP_EXTRACT="PCInstance_$CUR_DATE" # Directory to create and will contain the contents of extracted zip file +ALL_PC_INSTANCES_FOLDER="../target/instances" # Parent folder within the endtoend target folder for where all extracted instances should go + + +BUILT_ZIP_LOCATION="../../standalone/target" # Path to phenomecentral.org's build standalone/target folder, where standalone zip is located. Relative to scripts folder. +BUILT_ZIP_REGEX="phenomecentral-standalone*.zip" +BUILT_ZIP_EXACT_NAME="" # The exact name of the zip file found, to be set in extractZip() + + +OUTPUT_LOG_FILE="" # Mutate to absolute dir path to file in main +START_PC_COMMAND="./start.sh" +STOP_PC_COMMAND="./stop.sh" +START_SMTP_COMMAND="java -jar smtp-server/MockMock.jar" +SMTP_PID="" # PID of the FakeSMTP that is to be set in startSMTP() +READY_MESSAGE="About PhenomeCentral" +# Ensure that these relative paths remain when changing file structure of project +POM_LOCATION="../../../../pom.xml" # Relative to the extracted PC instance folder (target/instances/$ZIP_EXTRACT/phenomecentral-standalone-*/) +TESTNG_XML_LOCATION="src/test/java/org/phenotips/endtoendtests/testcases/xml/AllTests.xml" + +########################### +# Functions +########################### + +# cd into standalone directory, locate the zip, and extract it to where we were previously. If 0 or more than 1 standalone zip located, exits +extractZip() { + echo -e "\n====================== Extract Zip File ======================" + # Go to distribution folder of PC and check for zips there. + # Should do this because ls might give full path of file (instead of just filename) if we do not cd into the directory. Dependent on unix flavour. + cd "$BUILT_ZIP_LOCATION" + + # ls giving filenames only sorted by most recently modified descending + local ZIPS_FOUND_COUNT=$(ls $BUILT_ZIP_REGEX -t1 | wc -l) + BUILT_ZIP_EXACT_NAME=$(ls $BUILT_ZIP_REGEX -t1 | head -n 1) + + if [[ $ZIPS_FOUND_COUNT -eq 0 ]]; then + echo "No zips following pattern of $BUILT_ZIP_REGEX were found in $BUILT_ZIP_LOCATION. Exiting." + exit 1 + elif [[ $ZIPS_FOUND_COUNT -gt 1 ]]; then + echo "More than one zip following pattern of $BUILT_ZIP_REGEX were found in $BUILT_ZIP_LOCATION. Not sure which one to use. Exiting." + exit 2 + else + echo "Found $BUILT_ZIP_EXACT_NAME in $BUILT_ZIP_LOCATION" + # Return to where we were + cd - + + mkdir -p "$ALL_PC_INSTANCES_FOLDER/$ZIP_EXTRACT" + unzip "$BUILT_ZIP_LOCATION/$BUILT_ZIP_EXACT_NAME" -d "$ALL_PC_INSTANCES_FOLDER/$ZIP_EXTRACT" + + echo "Extracted $BUILT_ZIP_EXACT_NAME to $ALL_PC_INSTANCES_FOLDER/$ZIP_EXTRACT" + fi +} + +startInstance() { + echo -e "\n====================== Start PC Instance ======================" + + ZIP_SUBDIR=${BUILT_ZIP_EXACT_NAME%????} # Cut off last 4 chars of BUILT_ZIP_EXACT_NAME (remove the .zip extension as this is the folder name) + cd $ALL_PC_INSTANCES_FOLDER/$ZIP_EXTRACT/$ZIP_SUBDIR + echo "Starting server on port $PC_INSTANCE_PORT and stop port $PC_INSTANCE_STOP_PORT" + $START_PC_COMMAND $PC_INSTANCE_PORT $PC_INSTANCE_STOP_PORT & + sleep 30 + echo "Waited 30 seconds for server to start. Now check with curl command" +} + +# Checks if the instance has started, recursivly calls itself to check again if the "Phenotips is initializing" message is still there after waiting. +checkForStart() { + echo -e "\n====================== Check PC Instance Start Status ======================" + local CURL_RESULT + local CURL_RETURN + CURL_RESULT=$(curl "$PC_INSTANCE_URL") + CURL_RETURN=$? # "local" affects the return code of above curl command. Declarae vars first. + + if test "$CURL_RETURN" != "0"; then + echo "Curl to $PC_INSTANCE_URL has failed. Wait 10 secs on try again" + sleep 10 + checkForStart + else + echo "Response recieved on curl to $PC_INSTANCE_URL." + local READY_MESSAGE_FOUND=$(echo "$CURL_RESULT" | grep -c "$READY_MESSAGE") + + if [[ "$READY_MESSAGE_FOUND" -gt 0 ]]; then + echo "It seems instance has sucessfully started and is ready since '$READY_MESSAGE' is visible on the page" + else + echo "Instance is not ready yet. The string $READY_MESSAGE was not found on page. Wait 10 seconds and ping again" + sleep 10 + checkForStart + fi + fi +} + +runTests() { + echo -e "\n====================== Running Selenium Tests ======================" + echo "Compiling and running e2e testing framework with maven. Should see maven messages and browser soon" + mvn test -f $POM_LOCATION -Dsurefire.suiteXmlFiles=$TESTNG_XML_LOCATION -Dbrowser=$BROWSER -DhomePageURL=$PC_INSTANCE_URL -DemailUIPageURL=$EMAIL_UI_URL +} + +stopInstance() { + echo -e "\n====================== Stopping PC Instance ======================" + echo "Stopping instance that was started on port $PC_INSTANCE_PORT and stop port $PC_INSTANCE_STOP_PORT" + $STOP_PC_COMMAND $PC_INSTANCE_STOP_PORT +} + +startSMTP() { + echo -e "\n====================== Start Mock SMTP ======================" + echo "Starting SMTP (MockMock). Listening on $OUTGOING_EMAIL_PORT. Email UI is at $EMAIL_UI_PORT" + $START_SMTP_COMMAND -p $OUTGOING_EMAIL_PORT -h $EMAIL_UI_PORT & + SMTP_PID=$! + echo "DEBUG: PID of SMTP is: $SMTP_PID" + sleep 10 +} + +stopSMTP() { + echo -e "\n====================== Stop SMTP ======================" + echo "Killing SMTP with PID of $SMTP_PID" + while kill INT $SMTP_PID 2>/dev/null; do + sleep 1 + done +} + +onCtrlC() { + echo "Ctrl C recieved. Stopping script" + stopInstance + stopSMTP + exit 3 +} + +checkPWD() { + # Taken from start.sh + # Ensure that the commands below are always started in the directory where this script is + # located. To do this we compute the location of the current script. + PRG="$0" + while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi + done + PRGDIR=`dirname "$PRG"` + cd "$PRGDIR" + + echo "ProgramDir is calculated as: $PRGDIR" +} + +printHelp() { + echo "Usage: $0 [--argName argValue]" + echo "Example: $0 --browser chrome --start 8083 --stop 8084 --emailUI 8085 --emailListen 1025" + echo "Arguments can be passed in any order and any number of them can be used" + echo "The example shows the default values if the arg is not supplied." + echo "" + echo "Possible Arguments:" + echo "--help (or -h) display this help message" + echo "--browser Run tests with browser specified. Must be one of: chrome, firefox, edge, ie, safari" + echo "--start Start port of the PC instance" + echo "--stop Stop port of the PC instance" + echo "--emailUI Email UI (MockMock SMTP) access port" + echo "--emailListen Port that the mock SMTP listens to for messages. This is the port that the PC instance sends outgoing emails." +} + +# Help from https://stackoverflow.com/questions/16483119/an-example-of-how-to-use-getopts-in-bash +parseArgs() { + echo -e "\n====================== Parsing Arguments ======================" + + OPTSPEC=":h-:" + while getopts "$OPTSPEC" OPTCHAR; do + case "${OPTCHAR}" in + -) + VAL="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 )) + echo "Parsing option: '--${OPTARG}', value: '${VAL}'" >&2; + case "${OPTARG}" in + help) + printHelp + exit 0 + ;; + browser) + BROWSER=${VAL} + echo "Browser is being specified as: $BROWSER" + ;; + start) + PC_INSTANCE_PORT=${VAL} + echo "PC instance start port is being specified as: $PC_INSTANCE_PORT" + ;; + stop) + PC_INSTANCE_STOP_PORT=${VAL} + echo "PC instance stop port is being specified as: $PC_INSTANCE_STOP_PORT" + ;; + emailUI) + EMAIL_UI_PORT=${VAL} + echo "Email UI port is being specified as: $EMAIL_UI_PORT" + ;; + emailListen) + OUTGOING_EMAIL_PORT=${VAL} + echo "Email listening port is being specified as: $OUTGOING_EMAIL_PORT" + ;; + *) + if [ "$OPTERR" = 1 ] && [ "${OPTSPEC:0:1}" != ":" ]; then + echo "Unknown option --${OPTARG}" >&2 + fi + ;; + esac;; + h) + printHelp + exit 0 + ;; + *) + if [ "$OPTERR" != 1 ] || [ "${OPTSPEC:0:1}" = ":" ]; then + echo "Non-option argument: '-${OPTARG}'" >&2 + fi + ;; + esac + done + +} + + + +################## +# main +################## +echo -e "\n====================== $0 Start ======================" + +checkPWD +parseArgs $@ # Pass command line params to parseArgs... important + +# Argument parsing might have changed these. +PC_INSTANCE_URL="http://localhost:$PC_INSTANCE_PORT" +EMAIL_UI_URL="http://localhost:$EMAIL_UI_PORT" + +mkdir -p $ALL_PC_INSTANCES_FOLDER + +# Create a debug log file. +OUTPUT_LOG_FILE="$PWD/$ALL_PC_INSTANCES_FOLDER/outputLog_$CUR_DATE.txt" #Specify absolute path to OUTPUT_LOG_FILE +touch $OUTPUT_LOG_FILE + +# Capture both stderr and stout to OUTPUT_LOG_FILE. +# Note: This is process substitution, it might not work on non-bash shells such as ksh or sh. +exec > >(tee -ia $OUTPUT_LOG_FILE) +exec 2> >(tee -ia $OUTPUT_LOG_FILE >&2) + +extractZip + +trap onCtrlC SIGINT # If we break (ctrl+c) from here on, we should stop SMTP and PC instance + +startSMTP +startInstance +checkForStart +runTests +stopInstance +stopSMTP + +echo -e "\n====================== Generate Allure Report ======================" +mvn -f $POM_LOCATION io.qameta.allure:allure-maven:report + +echo -e "\n====================== $0 End ======================" diff --git a/end-to-end-tests/scripts/smtp-server/MockMock.jar b/end-to-end-tests/scripts/smtp-server/MockMock.jar new file mode 100644 index 00000000..12ed6c25 Binary files /dev/null and b/end-to-end-tests/scripts/smtp-server/MockMock.jar differ diff --git a/end-to-end-tests/scripts/smtp-server/Readme.md b/end-to-end-tests/scripts/smtp-server/Readme.md new file mode 100644 index 00000000..c8b2106e --- /dev/null +++ b/end-to-end-tests/scripts/smtp-server/Readme.md @@ -0,0 +1,7 @@ +# Fake SMTP Server - MockMock + +`MockMock.jar` is taken from https://github.com/tweakers/MockMock + +This provides a fake SMTP service, including a UI for an email inbox that the tests can use. We can move this to a custom maven dependency to pull from github each time if need be. Otherwise, a copy of the jar will remain here which is the easiest solution. + +This is program was provided under the Apache License 2.0 license: https://github.com/tweakers/MockMock/blob/master/LICENSE diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/DummyTest.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/DummyTest.java new file mode 100644 index 00000000..1a023ec4 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/DummyTest.java @@ -0,0 +1,68 @@ +package org.phenotips.endtoendtests; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.testng.annotations.Test; + +// This is a self-contained test that does not depend on anything in org.phenotips.endtoendtests.pageobjects +// Implement using JUnit or TestNG for annotations +// Seperate into org.phenotips.endtoendtests.pageobjects and Testcases + +public class DummyTest +{ + final String url = "http://localhost:8083/"; + + final String username = "TestUser2Dos"; + + final String pass = "123456"; + + final By loginLink = By.id("launch-login"); + + final By userNameField = By.id("j_username"); + + final By passField = By.id("j_pasword"); // TODO: There might be a typo there + + //final By loginButton = By.xpath("/html/body/div/div/div[2]/div/div[1]/div/div/div/div[2]/div[2]/form/div[2]/input"); + final By loginButton = By.cssSelector("input.button"); + + final By logOutLink = By.id("tmLogout"); + + /* + * Logs in and asserts that "Log Out" is somewhere on the page" + * */ + @Test + public void loginTest() + { + WebDriver theDriver = new FirefoxDriver(); + WebDriverWait wait = new WebDriverWait(theDriver, 2); + + theDriver.navigate().to(url); + + wait.until(ExpectedConditions.presenceOfElementLocated(loginLink)); + theDriver.findElement(loginLink).click(); + + wait.until(ExpectedConditions.presenceOfElementLocated(userNameField)); + theDriver.findElement(userNameField).click(); + theDriver.findElement(userNameField).sendKeys(username); + + theDriver.findElement(passField).click(); + theDriver.findElement(passField).sendKeys(pass); + + theDriver.findElement(loginButton).click(); + + // Assert true on JUnit here. + wait.until(ExpectedConditions.presenceOfElementLocated(logOutLink)); + theDriver.findElement(logOutLink); + + // Pause for demo video + try { + Thread.sleep(1500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + theDriver.quit(); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/SetupUsers.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/SetupUsers.java new file mode 100644 index 00000000..de6188b4 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/SetupUsers.java @@ -0,0 +1,53 @@ +package org.phenotips.endtoendtests; + +import org.phenotips.endtoendtests.pageobjects.HomePage; +import org.phenotips.endtoendtests.testcases.BaseTest; + +import org.testng.annotations.Test; + +/** + * This class sets up the state of the PC instance for testing purposes. Notably, it creates the two main users that all + * other tests will be using, along with setting an outgoing email port corresponding to the one that the fake SMTP + * service is listening to. + */ +public class SetupUsers extends BaseTest +{ + HomePage aHomePage = new HomePage(theDriver); + + @Test() + public void setEmailPort() + { + aHomePage.navigateToLoginPage() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToMailSendingSettingsPage() + .setEmailPort(1025) + .navigateToHomePage() + .logOut(); + } + + // Creates the two users used by the automation. + @Test() + public void setupAutomationUsers() + { + aHomePage.navigateToLoginPage() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToAdminUsersPage() + .addUser("TestUser1", "Uno", "123456", + "testuser1uno@jksjfljsdlfj.caksjdfjlkg", "none", + "Test server", "Some reason") + .addUser("TestUser2", "Dos", "123456", + "testuser2dos@kljaskljdfljlfd.casdfjjg", "none", + "Test server", "Some reason") + .navigateToPendingUsersPage() + .approvePendingUser("TestUser1Uno") + .approvePendingUser("TestUser2Dos") + .logOut() + .loginAsUser() + .logOut() + .loginAsUserTwo() + .logOut(); + System.out.println("Created Two users for automation"); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/common/CommonInfoEnums.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/common/CommonInfoEnums.java new file mode 100644 index 00000000..38cfa206 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/common/CommonInfoEnums.java @@ -0,0 +1,40 @@ +package org.phenotips.endtoendtests.common; + +/** + * This class provides common enums used by both org.phenotips.endtoendtests.pageobjects and in org.phenotips.endtoendtests.test cases + * too. This level of indirection is actually optional for our usage. It allows for the headings name to change and then + * we can update the enum without touching the selector. + */ +public interface CommonInfoEnums +{ + // Public enum for friendly names + enum SECTIONS + { + PatientInfoSection, + FamilyHistorySection, + PrenatalHistorySection, + MedicalHistorySection, + MeasurementSection, + ClinicalSymptomsSection, + SuggestedGenesSection, + GenotypeInfoSection, + DiagnosisSection, + SimilarCasesSection + } + + // Privilage levels + enum PRIVILAGE + { + CanView, + CanViewAndModify, + CanViewAndModifyAndManageRights + } + + // Visibility Levels + enum VISIBILITY + { + Private, + Matchable, + Public + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/common/CommonPatientMeasurement.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/common/CommonPatientMeasurement.java new file mode 100644 index 00000000..a4ed3025 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/common/CommonPatientMeasurement.java @@ -0,0 +1,134 @@ +package org.phenotips.endtoendtests.common; + +import java.util.Objects; + +/** + * This class acts as a public struct for patient measurements, too many fields to just pass as a List of Strings. All + * the possible numerical fields for a patient's measurements should go here. + */ +public class CommonPatientMeasurement +{ + public float weight; + + public float armSpan; + + public float headCircumference; + + public float outerCanthalDistance; + + public float leftHandLength; + + public float rightHandLength; + + public float height; + + public float sittingHeight; + + public float philtrumLength; + + public float inntercanthalDistance; + + public float leftPalmLength; + + public float rightPalmLength; + + public float leftEarLength; + + public float palpebralFissureLength; + + public float leftFootLength; + + public float rightFootLength; + + public float rightEarLength; + + public float interpupilaryDistance; + + // Ctor + public CommonPatientMeasurement(float weight, float armSpan, float headCircumference, float outerCanthalDistance, + float leftHandLength, float rightHandLength, float height, float sittingHeight, float philtrumLength, + float inntercanthalDistance, float leftPalmLength, float rightPalmLength, float leftEarLength, + float palpebralFissureLength, float leftFootLength, float rightFootLength, float rightEarLength, + float interpupilaryDistance) + { + this.weight = weight; + this.armSpan = armSpan; + this.headCircumference = headCircumference; + this.outerCanthalDistance = outerCanthalDistance; + this.leftHandLength = leftHandLength; + this.rightHandLength = rightHandLength; + this.height = height; + this.sittingHeight = sittingHeight; + this.philtrumLength = philtrumLength; + this.inntercanthalDistance = inntercanthalDistance; + this.leftPalmLength = leftPalmLength; + this.rightPalmLength = rightPalmLength; + this.leftEarLength = leftEarLength; + this.palpebralFissureLength = palpebralFissureLength; + this.leftFootLength = leftFootLength; + this.rightFootLength = rightFootLength; + this.rightEarLength = rightEarLength; + this.interpupilaryDistance = interpupilaryDistance; + } + + @Override public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CommonPatientMeasurement that = (CommonPatientMeasurement) o; + return Float.compare(that.weight, weight) == 0 && + Float.compare(that.armSpan, armSpan) == 0 && + Float.compare(that.headCircumference, headCircumference) == 0 && + Float.compare(that.outerCanthalDistance, outerCanthalDistance) == 0 && + Float.compare(that.leftHandLength, leftHandLength) == 0 && + Float.compare(that.rightHandLength, rightHandLength) == 0 && + Float.compare(that.height, height) == 0 && + Float.compare(that.sittingHeight, sittingHeight) == 0 && + Float.compare(that.philtrumLength, philtrumLength) == 0 && + Float.compare(that.inntercanthalDistance, inntercanthalDistance) == 0 && + Float.compare(that.leftPalmLength, leftPalmLength) == 0 && + Float.compare(that.rightPalmLength, rightPalmLength) == 0 && + Float.compare(that.leftEarLength, leftEarLength) == 0 && + Float.compare(that.palpebralFissureLength, palpebralFissureLength) == 0 && + Float.compare(that.leftFootLength, leftFootLength) == 0 && + Float.compare(that.rightFootLength, rightFootLength) == 0 && + Float.compare(that.rightEarLength, rightEarLength) == 0 && + Float.compare(that.interpupilaryDistance, interpupilaryDistance) == 0; + } + + @Override public int hashCode() + { + return Objects + .hash(weight, armSpan, headCircumference, outerCanthalDistance, leftHandLength, rightHandLength, height, + sittingHeight, philtrumLength, inntercanthalDistance, leftPalmLength, rightPalmLength, leftEarLength, + palpebralFissureLength, leftFootLength, rightFootLength, rightEarLength, interpupilaryDistance); + } + + @Override public String toString() + { + return "CommonPatientMeasurement{" + + "weight=" + weight + + ", armSpan=" + armSpan + + ", headCircumference=" + headCircumference + + ", outerCanthalDistance=" + outerCanthalDistance + + ", leftHandLength=" + leftHandLength + + ", rightHandLength=" + rightHandLength + + ", height=" + height + + ", sittingHeight=" + sittingHeight + + ", philtrumLength=" + philtrumLength + + ", inntercanthalDistance=" + inntercanthalDistance + + ", leftPalmLength=" + leftPalmLength + + ", rightPalmLength=" + rightPalmLength + + ", leftEarLength=" + leftEarLength + + ", palpebralFissureLength=" + palpebralFissureLength + + ", leftFootLength=" + leftFootLength + + ", rightFootLength=" + rightFootLength + + ", rightEarLength=" + rightEarLength + + ", interpupilaryDistance=" + interpupilaryDistance + + '}'; + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminEmailSendingSettingsPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminEmailSendingSettingsPage.java new file mode 100644 index 00000000..a9119f67 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminEmailSendingSettingsPage.java @@ -0,0 +1,38 @@ +package org.phenotips.endtoendtests.pageobjects; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import io.qameta.allure.Step; + +/** + * This class corresponds to the Mail Sending Settings page that is found in Admin settings under the left accordion + * menu: Email -> Mail Sending (I.e. http://localhost:8083/admin/XWiki/XWikiPreferences?editor=globaladmin§ion=emailSend) + */ +public class AdminEmailSendingSettingsPage extends AdminSettingsPage +{ + private final By emailServerPortBox = By.id("Mail.SendMailConfigClass_0_port"); + + private final By saveBtn = By.cssSelector("input[value='Save']"); + + public AdminEmailSendingSettingsPage(WebDriver aDriver) + { + super(aDriver); + } + + /** + * Sets the email port of the PC instance to the one specified. Clears whatever is in the box and sets it. Requires + * that the Email Sending Settings page be open. + * + * @param port is desired port, as number, to direct outgoing emails to. + * @return Stay on the same page so return the same object. + */ + @Step("Set the outgoing email port to: {0}") + public AdminEmailSendingSettingsPage setEmailPort(int port) + { + clickAndClearElement(emailServerPortBox); + clickAndTypeOnElement(emailServerPortBox, Integer.toString(port)); + clickOnElement(saveBtn); + return this; + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminMatchNotificationPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminMatchNotificationPage.java new file mode 100644 index 00000000..7710be89 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminMatchNotificationPage.java @@ -0,0 +1,218 @@ +package org.phenotips.endtoendtests.pageobjects; + +import java.util.List; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; + +import io.qameta.allure.Step; + +/** + * The admin page where match notifications can be sent. Administration -> PhenoTips -> Matching Notification in the + * left accordion menu i.e. http://localhost:8083/admin/XWiki/XWikiPreferences?editor=globaladmin§ion=Matching+Notification + */ +public class AdminMatchNotificationPage extends AdminSettingsPage +{ + private final By patientIDContainsBox = By.id("external-id-filter"); + + private final By reloadMatchesBtn = By.id("show-matches-button"); + + private final By firstRowFirstEmailBox = By.cssSelector("#matchesTable td[name=referenceEmails] > input.notify"); + + private final By firstRowSecondEmailBox = By.cssSelector("#matchesTable td[name=matchedEmails] > input.notify"); + + private final By sendNotificationsBtn = By.id("send-notifications-button"); + + private final By referencePatientLink = By.cssSelector("#referencePatientTd > a.patient-href"); + + private final By matchedPatientLink = By.cssSelector("#matchedPatientTd > a.patient-href"); + + private final By contactedStatusCheckbox = By.cssSelector("input[name=notified-filter][value=notified]"); + + private final By notContactedStatusCheckbox = By.cssSelector("input[name=notified-filter][value=unnotified]"); + + private final By sendingNotificationMessage = By.cssSelector("#send-notifications-messages > div"); + + private final By matchesGenotypeScoreSlider = By.cssSelector("#show-matches-gen-score > div.handle"); + + private final By matchesAverageScoreSlider = By.cssSelector("#show-matches-score > div.handle"); + + public AdminMatchNotificationPage(WebDriver aDriver) + { + super(aDriver); + } + + /** + * Filters the table by inputting a string into "Patient ID contains:" box and clicking "Refresh Matches" + * + * @param identifier String to search by. Usually an identifier or the Patient ID itself + * @return the same object as we are still on the same page, just with the table filtered + */ + @Step("Filter matches by identifier: {0}") + public AdminMatchNotificationPage filterByID(String identifier) + { + clickAndTypeOnElement(patientIDContainsBox, identifier); + clickOnElement(reloadMatchesBtn); + waitForLoadingBarToDisappear(); + return this; + } + + /** + * Notifies the two users on the first row of a match. Requires that there is at least one visible row on the table. + * Waits until the green "Sending request..." text beside the Send Notifications button disappears Note that the PC + * instance should have email configured as this does not check the actual text that appears after clicking on the + * Send Notifcations button. + * + * @return the same (current) object, as we stay on the same page. + */ + @Step("Send an email to the matched patients in the first row") + public AdminMatchNotificationPage emailFirstRowUsers() + { + clickOnElement(firstRowFirstEmailBox); + clickOnElement(firstRowSecondEmailBox); + clickOnElement(sendNotificationsBtn); + waitForElementToBePresent(sendingNotificationMessage); + waitForElementToBeGone(sendingNotificationMessage); + return this; + } + + /** + * Emails the specified patients in the matching table. Each patient must be in their respective column. Searches + * for the patient using the same behaviour as the "Patient ID contains:" filter box. i.e. can pass a substring of + * the patient ID or Patient Name. Will take the first match where the respective substrings appear. + * + * @param referencePatient The patient name, or ID number, in the Reference Column + * @param matchedPatient The patient name, or ID number, in the Matched Column, on the same row as + * referencePatient. + * @return Stay on the same page so return the same object. + */ + @Step("Find and email specific matched patients. Reference Patient ID: {0} with Matched Patient ID: {1}") + public AdminMatchNotificationPage emailSpecificPatients(String referencePatient, String matchedPatient) + { + filterByID(referencePatient); + waitForLoadingBarToDisappear(); + + List loFoundReferencePatients = superDriver.findElements(referencePatientLink); + List loFoundMatchedPatients = superDriver.findElements(matchedPatientLink); + List loFoundReferenceEmailBoxes = superDriver.findElements(firstRowFirstEmailBox); + List loFoundMatchedEmailBoxes = superDriver.findElements(firstRowSecondEmailBox); + + System.out.println("Found reference email boxes number: " + loFoundReferenceEmailBoxes.size()); + System.out.println("Found matched email boxes number: " + loFoundMatchedEmailBoxes.size()); + + for (int i = 0; i < loFoundMatchedPatients.size(); ++i) { + System.out.println("For loop: Reference: " + loFoundMatchedPatients.get(i).getText() + + "Matched patient: " + loFoundReferencePatients.get(i).getText()); + if (loFoundMatchedPatients.get(i).getText().contains(matchedPatient) && + loFoundReferencePatients.get(i).getText().contains(referencePatient)) + { + clickOnElement(loFoundReferenceEmailBoxes.get(i)); + clickOnElement(loFoundMatchedEmailBoxes.get(i)); + System.out.println("Found a match"); + break; + } + } + + clickOnElement(sendNotificationsBtn); + + // Wait for the green "Sending emails..." message to disappear. + waitForElementToBeGone(sendingNotificationMessage); + + return this; + } + + /** + * Determines if the two specified patients appear on the match table, matching to each other. + * + * @param referencePatient The reference patient, either PatientID or unique identifier, can be substring. + * @param matchedPatient The matched patient, either patientID or unique identifier, can be substring. + * @return boolean, true when there is a referencePatient matching to the matchedPatient, false if match not found. + */ + @Step("Does match exist between Reference Patient ID: {0} and Matched patient ID: {0}") + public boolean doesMatchExist(String referencePatient, String matchedPatient) + { + filterByID(referencePatient); + waitForLoadingBarToDisappear(); + + List loFoundReferencePatients = superDriver.findElements(referencePatientLink); + List loFoundMatchedPatients = superDriver.findElements(matchedPatientLink); + + for (int i = 0; i < loFoundMatchedPatients.size(); ++i) { + System.out.println("For loop: Reference: " + loFoundMatchedPatients.get(i).getText() + + "Matched patient: " + loFoundReferencePatients.get(i).getText()); + if (loFoundMatchedPatients.get(i).getText().contains(matchedPatient) && + loFoundReferencePatients.get(i).getText().contains(referencePatient)) + { + return true; + } + } + + return false; // didn't find the specified patients while looping through match table + } + + /** + * Toggles the "Contacted status: contacted" filter checkbox. + * + * @return Stay on the same page so return the same object. + */ + @Step("Toggle contacted status checkbox") + public AdminMatchNotificationPage toggleContactedStatusCheckbox() + { + clickOnElement(contactedStatusCheckbox); + return this; + } + + /** + * Toggles the "Contacted status: not contacted" filter checkbox. + * + * @return Stay on the same page so return the same object. + */ + @Step("Toggle not contacted status checkbox") + public AdminMatchNotificationPage toggleNotContactedStatusCheckbox() + { + clickOnElement(notContactedStatusCheckbox); + return this; + } + + /** + * Sets the genotype slider to 0 by dragging all the way to the left. + * + * @return Stay on the same page so return the same object. + */ + @Step("Set genotype slider filter to zero") + public AdminMatchNotificationPage setGenotypeSliderToZero() + { + waitForElementToBePresent(matchesGenotypeScoreSlider); + + Actions actionBuilder = new Actions(superDriver); + actionBuilder.dragAndDropBy(superDriver.findElement(matchesGenotypeScoreSlider), -50, 0) + .build().perform(); + System.out.println("Dragging Genotype score slider to 0."); + + clickOnElement(reloadMatchesBtn); + + return this; + } + + /** + * Sets the average score to the minimum value by sliding the average score slider all the way to the left. + * + * @return Stay on the same page so return the same object. + */ + @Step("Set the Average Score slider filter to the minimum value (hard left)") + public AdminMatchNotificationPage setAverageScoreSliderToMinimum() + { + waitForElementToBePresent(matchesAverageScoreSlider); + + Actions actionBuilder = new Actions(superDriver); + actionBuilder.dragAndDropBy(superDriver.findElement(matchesAverageScoreSlider), -50, 0) + .build().perform(); + System.out.println("Dragging Average Score slider to 0.1"); + + clickOnElement(reloadMatchesBtn); + + return this; + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminPendingUsersPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminPendingUsersPage.java new file mode 100644 index 00000000..9e6146c6 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminPendingUsersPage.java @@ -0,0 +1,69 @@ +package org.phenotips.endtoendtests.pageobjects; + +import java.util.ArrayList; +import java.util.List; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import io.qameta.allure.Step; + +/** + * This class corresponds to the admin's Pending Users page, where users can be approved for access to PC. I.e. + * http://localhost:8083/admin/XWiki/XWikiPreferences?editor=globaladmin§ion=PendingUsers + */ +public class AdminPendingUsersPage extends AdminSettingsPage +{ + private final By approveUserBtns = By.cssSelector("#admin-page-content td.manage > img:nth-child(1)"); + + private final By usernamesLinks = By.cssSelector("#userstable td.username > a"); + + public AdminPendingUsersPage(WebDriver aDriver) + { + super(aDriver); + } + + /** + * Approves the nth pending user on the pending users table. Requires there is at least one pending user waiting to + * be approved. + * + * @param n is the Nth user in the table, should be >= 1. + * @return Stay on the same page so return the same object. + */ + @Step("Approve the {0}th pending user on the list") + public AdminPendingUsersPage approveNthPendingUser(int n) + { + waitForElementToBePresent(approveUserBtns); // Should wait for first button to appear. + clickOnElement(superDriver.findElements(approveUserBtns).get(n - 1)); + + superDriver.switchTo().alert().accept(); + superDriver.switchTo().defaultContent(); + + return this; + } + + /** + * Approves the pending user that is specified by the username. Requires: That the username is valid and exists + * within the table of pending users. + * + * @param userName is username of pending user as a String. + * @return Stay on the same page so return the same object. + */ + @Step("Approve pending user with username: {0}") + public AdminPendingUsersPage approvePendingUser(String userName) + { + waitForElementToBePresent(approveUserBtns); + + List usernamesFound = new ArrayList<>(); + superDriver.findElements(usernamesLinks).forEach(x -> usernamesFound.add(x.getText())); + + int indexOfUsername = usernamesFound.indexOf(userName); + + clickOnElement(superDriver.findElements(approveUserBtns).get(indexOfUsername)); + + superDriver.switchTo().alert().accept(); + superDriver.switchTo().defaultContent(); + + return this; + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminRefreshMatchesPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminRefreshMatchesPage.java new file mode 100644 index 00000000..10b1aeeb --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminRefreshMatchesPage.java @@ -0,0 +1,125 @@ +package org.phenotips.endtoendtests.pageobjects; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import io.qameta.allure.Step; + +/** + * This class represents the Admin's Refresh Matches page where they can refresh matches for All Patients or Patients + * Modified Since Last Update. + */ +public class AdminRefreshMatchesPage extends BasePage +{ + private final By patientsSinceLastModBtn = By.id("find-updated-matches-button"); + + private final By allMatchesBtn = By.id("find-all-matches-button"); + + private final By confirmFindAllMatchesBtn = By.cssSelector("input.button[value=\"Find matches\"]"); + + // Assumes local server only, i.e. There is only one row. + private final By selectLocalServerForMatchBox = By.cssSelector("td.select-for-update > input"); + + private final By numberPatientsProcessed = By.cssSelector("td.numPatientsCheckedForMatches"); + + private final By totalMatchesFound = By.cssSelector("td.totalMatchesFound"); + + private final By findMatchesText = By.id("find-matches-messages"); + + private final String completedMatchesMessage = "Done - refresh page to see the updated table above."; + + public AdminRefreshMatchesPage(WebDriver aDriver) + { + super(aDriver); + } + + /** + * Hits the refresh matches "For all patients" button and then calls waitForSucessMessage() to wait for the green + * sucess message text. + * + * @return Stay on the same page so we return the same object. + */ + @Step("Refresh matches for All Patients") + public AdminRefreshMatchesPage refreshAllMatches() + { + clickOnElement(selectLocalServerForMatchBox); + clickOnElement(allMatchesBtn); + clickOnElement(confirmFindAllMatchesBtn); + waitForElementToBePresent(findMatchesText); // Must wait for this to appear before passing to loop. + waitForSucessMessage(); + + superDriver.navigate().refresh(); + waitForElementToBePresent(selectLocalServerForMatchBox); + + return this; + } + + /** + * Hits the refresh matches "For patients modified since last update" button. Calls the helper + * waitForSucessMessage() to wait for the green success text to appear. + * + * @return Stay on the same page so we return the same object. + */ + @Step("Refresh matches for patients that were modified since last update") + public AdminRefreshMatchesPage refreshMatchesSinceLastUpdate() + { + clickOnElement(selectLocalServerForMatchBox); + clickOnElement(patientsSinceLastModBtn); + waitForElementToBePresent(findMatchesText); // Must wait for this to appear before passing to loop. + waitForSucessMessage(); + + superDriver.navigate().refresh(); + unconditionalWaitNs(1); // It might find the element before refresh is completed. + waitForElementToBePresent(selectLocalServerForMatchBox); + + return this; + } + + /** + * Gets the number under the "Number of local patients processed" column. Assumes that there is only one row, gets + * only the first row. String because the value could be "-" for never having a refresh before. + * + * @return a STRING indicating the number of patients processed in the refresh. + */ + @Step("Get the number of local patients processed") + public String getNumberOfLocalPatientsProcessed() + { + waitForElementToBePresent(numberPatientsProcessed); + return superDriver.findElement(numberPatientsProcessed).getText(); + } + + /** + * Gets the number under "Total matches found" column. Assumes one row, only gets the value in the first row. String + * because value could be "-" when there was never a refresh. + * + * @return a STRING indicating the number of patients found during the refresh. + */ + @Step("Get the number of total matches found") + public String getTotalMatchesFound() + { + waitForElementToBePresent(totalMatchesFound); + return superDriver.findElement(totalMatchesFound).getText(); + } + + /** + * Helper function to wait for the sucessful green message text ("Done - refresh page to see...") Loop waits 5 + * seconds each time until the timeout of 30 seconds. Requires: Some green message text/matches search to be in + * progress. + */ + @Step("Wait for refresh sucess message 'Done - refresh page to see..'") + private void waitForSucessMessage() + { + int secondsWaited = 0; + int secondsToWait = 30; + + // While the green matches message text is not "Done, refresh page to see...", wait 5 secs and check again. + while (!(superDriver.findElement(findMatchesText).getText().equals(completedMatchesMessage))) { + if (secondsWaited > secondsToWait) { + break; + } + + unconditionalWaitNs(5); + secondsWaited += 5; + } + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminSettingsPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminSettingsPage.java new file mode 100644 index 00000000..4e6f8c52 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminSettingsPage.java @@ -0,0 +1,88 @@ +package org.phenotips.endtoendtests.pageobjects; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import io.qameta.allure.Step; + +/** + * This is the main Global Administrator settings page. Reached by clicking on "Administrator" (gear icon) link on the + * top left of the navbar. Ex. http://localhost:8083/admin/XWiki/XWikiPreferences + */ +public class AdminSettingsPage extends BasePage +{ + private final By matchingNotificationMenu = By.id("vertical-menu-Matching Notification"); + + private final By refreshMatchesMenu = By.id("vertical-menu-Refresh matches"); + + private final By usersMenu = By.id("vertical-menu-Users"); + + private final By pendingUsersMenu = By.id("vertical-menu-PendingUsers"); + + private final By mailSendingMenu = By.id("vertical-menu-emailSend"); + + public AdminSettingsPage(WebDriver aDriver) + { + super(aDriver); + } + + /** + * Navigates to the "Matching Notification" page. On the left accordion menu: PhenoTips -> Matching Notification + * + * @return a MatchNotification page object. + */ + @Step("Navigate to Admin Matching Notification Page") + public AdminMatchNotificationPage navigateToMatchingNotificationPage() + { + clickOnElement(matchingNotificationMenu); + return new AdminMatchNotificationPage(superDriver); + } + + /** + * Navigates to the "Refresh matches" page. On the left accordion menu: PhenoTips -> Refresh matches + * + * @return a AdminRefreshMatches page object as we navigate there. + */ + @Step("Navigate to Admin Refresh Matches Page") + public AdminRefreshMatchesPage navigateToRefreshMatchesPage() + { + clickOnElement(refreshMatchesMenu); + return new AdminRefreshMatchesPage(superDriver); + } + + /** + * Navigates to the Users page. From the accordion menu on the left: Users & Groups -> Users + * + * @return an AdminUsersPage object as we navigate there. + */ + @Step("Navigate to Admin's Users Page") + public AdminUsersPage navigateToAdminUsersPage() + { + clickOnElement(usersMenu); + return new AdminUsersPage(superDriver); + } + + /** + * Navigates to the Pending Users page. From the accordion menu on the left: Users & Groups -> Pending Users + * + * @return an AdminPendingUsersPage as we navigate there. + */ + @Step("Navigate to Admin's Pending Users Page") + public AdminPendingUsersPage navigateToPendingUsersPage() + { + clickOnElement(pendingUsersMenu); + return new AdminPendingUsersPage(superDriver); + } + + /** + * Navigates to the Mail Sending settings page. From the left accordion menu: Email -> Mail Sending + * + * @return an AdminEmailSendingSettingsPage instance as we navigate there. + */ + @Step("Navigate to Admin Mail Sending Settings Page") + public AdminEmailSendingSettingsPage navigateToMailSendingSettingsPage() + { + clickOnElement(mailSendingMenu); + return new AdminEmailSendingSettingsPage(superDriver); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminUsersPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminUsersPage.java new file mode 100644 index 00000000..dd20b8ba --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AdminUsersPage.java @@ -0,0 +1,61 @@ +package org.phenotips.endtoendtests.pageobjects; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import io.qameta.allure.Step; + +/** + * This page corresponds to the Users page from Administrator Settings. We can add users here. I.e. + * http://localhost:8083/admin/XWiki/XWikiPreferences?section=Users + */ +public class AdminUsersPage extends AdminSettingsPage implements CommonSignUpSelectors +{ + private final By saveBtn = By.cssSelector("input[value='Save']"); + + private final By cancelBtn = By.cssSelector("input[value='Cancel changes since last save']"); + + public AdminUsersPage(WebDriver aDriver) + { + super(aDriver); + } + + /** + * Adds a user to the PC instance. Only adds it to the Users table, does not confirm and authorize the user. Uses + * the default username that is suggested by XWiki. + * + * @param firstName First Name as a String. + * @param lastName Last name as a String. + * @param password is password as a String. + * @param email is email for the user as a String. Should be either a dummy address or something that we can + * access. + * @param affiliation is the value for the Affiliation box as a String. + * @param referral value for the "How did you hear about/ Who referred you" box as a String. + * @param justification value for the "Why are you requesting access" box as a String. + * @return Stay on the same page so return the same object. + */ + @Step("Adds a user with the desired parameters First name: {0} Last name: {1} Password: {2} Email: {3} Affiliation: {4} Referrer {5} and Justification: {6}") + public AdminUsersPage addUser(String firstName, String lastName, String password, + String email, String affiliation, String referral, String justification) + { + clickOnElement(newUserBtn); + clickAndTypeOnElement(firstNameBox, firstName); + clickAndTypeOnElement(lastNameBox, lastName); + clickOnElement(userNameBox); + clickAndTypeOnElement(passwordBox, password); + clickAndTypeOnElement(confirmPasswordBox, password); + clickAndTypeOnElement(emailBox, email); + clickAndTypeOnElement(affiliationBox, affiliation); + clickAndTypeOnElement(referralBox, referral); + clickAndTypeOnElement(reasoningBox, justification); + + clickOnElement(professionalCheckbox); + clickOnElement(liabilityCheckbox); + clickOnElement(nonIdentificationCheckbox); + clickOnElement(cooperationCheckbox); + clickOnElement(acknoledgementCheckbox); + + clickOnElement(saveBtn); + return this; + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AllPatientsPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AllPatientsPage.java new file mode 100644 index 00000000..37a5e294 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/AllPatientsPage.java @@ -0,0 +1,122 @@ +package org.phenotips.endtoendtests.pageobjects; + +import java.util.List; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import io.qameta.allure.Step; + +/** + * Represents the http://localhost:8083/AllData page, where "Browse... -> Browse patients" is clicked on + */ +public class AllPatientsPage extends BasePage +{ + private final By importJSONLink = By.id("phenotips_json_import"); + + private final By JSONBox = By.id("import"); + + private final By importBtn = By.id("import_button"); + + private final By sortCreationDate = By.cssSelector( + "th.xwiki-livetable-display-header-text:nth-child(4) > a:nth-child(1)"); // 4th column + + private final By firstPatientRowLink = By.cssSelector( + "#patients-display > tr:nth-child(1) > td:nth-child(1) > a:nth-child(1)"); + + private final By deleteBtns = By.cssSelector("tbody[id=patients-display] > tr > td.actions > a.fa-remove"); + + private final By deleteYesConfirmBtn = By.cssSelector("input[value=Yes]"); + + private final By patientIDFilterBox = By.cssSelector("input[title=\"Filter for the Identifier column\"]"); + + public AllPatientsPage(WebDriver aDriver) + { + super(aDriver); + } + + /** + * Imports a patient via the passed JSON string. Waits 5 seconds before returning, difficult to detect when import + * is sucessful. + * + * @param theJSON a long string which represents the JSON. Ensure that backslashes are escaped. + * @return the same object, we stay on the same page. + */ + @Step("Import a patient via JSON {0}") + public AllPatientsPage importJSONPatient(String theJSON) + { + clickOnElement(importJSONLink); + clickAndTypeOnElement(JSONBox, theJSON); + clickOnElement(importBtn); + waitForInProgressMsgToDisappear(); + waitForLoadingBarToDisappear(); + return this; + } + + /** + * Sorts the list of patients in descending order. Assumes that the default sort (i.e. what comes up when the page + * is first visited) is the starting state. Clicks on the sort twice, needs to do that for some reason to sort + * descending. + * + * @return same object as it is the same page. + */ + @Step("Sort patients in descending date order") + public AllPatientsPage sortPatientsDateDesc() + { + clickOnElement(sortCreationDate); + clickOnElement(sortCreationDate); + waitForLoadingBarToDisappear(); + return this; + } + + /** + * Click on the first patient in the table to view its full profile. + * + * @return the patient's full info page which is called the ViewPatientPage + */ + @Step("View first patient in table") + public ViewPatientPage viewFirstPatientInTable() + { + clickOnElement(firstPatientRowLink); + return new ViewPatientPage(superDriver); + } + + /** + * Deletes all the patients in the table if there are any, only for the first page + * + * @return stay on the same page, so return the same object. + */ + @Step("Delete all patients on the table") + public AllPatientsPage deleteAllPatients() + { + waitForLoadingBarToDisappear(); + + List loDeleteBtns = superDriver.findElements(deleteBtns); + + // theElement just acts as an iterator for the array size, we don't use it. + for (WebElement theElement : loDeleteBtns) { // Use the original number of rows a counter + // theElement.click(); // Table pagintes upwards + clickOnElement(deleteBtns); + clickOnElement(deleteYesConfirmBtn); + waitForInProgressMsgToDisappear(); + waitForLoadingBarToDisappear(); + } + + return this; + } + + /** + * Filters by the patient ID by sending keys to the "type to filter" box under the identifier column + * + * @param patientID the patient ID to enter, should be in Pxxxxxxx format. + * @return stay on the same page so return the same object. + */ + @Step("Filter patient {0} by their identifier or patient ID") + public AllPatientsPage filterByPatientID(String patientID) + { + clickAndTypeOnElement(patientIDFilterBox, patientID); + waitForLoadingBarToDisappear(); + return this; + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/BasePage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/BasePage.java new file mode 100644 index 00000000..e71f9f2f --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/BasePage.java @@ -0,0 +1,539 @@ +package org.phenotips.endtoendtests.pageobjects; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.pagefactory.ByChained; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import io.qameta.allure.Step; + +/** + * This abstract class contains the toolbar (navbar) elements which is visible on all pages. All page classes should + * inherit this base class + */ +public abstract class BasePage +{ + /*************************************************************************************** + * Environment Information. You can change these as needed to run tests easily through + * IDE instead of passing in command line options to JVM. + ***************************************************************************************/ + /** + * Public selectors, the LoginPageTest touches these. Ideally, tests should not touch selectors. + */ + public final By adminLink = By.id("tmAdminSpace"); + + public final By aboutLink = By.id("tmAbout"); + + /** + * Common login credentials. + */ + protected final String ADMIN_USERNAME = "Admin"; + + protected final String ADMIN_PASS = "admin"; + + protected final String USER_USERNAME = "TestUser1Uno"; + + protected final String USER_PASS = "123456"; + + protected final String USER_USERNAME2 = "TestUser2Dos"; + + protected final String USER_PASS2 = "123456"; + + /******************************* + * Selectors + *******************************/ + protected final By logOutLink = By.id("tmLogout"); // Used in child classes to check when modals close. + + // Approval Pending Message that appears on all pages for an unapproved user + protected final By approvalPendingMessage = By.cssSelector("#mainContentArea > div.infomessage"); + + // PC logo at top left to navigate to homepage + protected final By phenomeCentralLogoBtn = By.cssSelector("#companylogo > a > img"); + + /** + * Default "maximum" waiting time in seconds. Notably it is used when waiting for an element to appear. This can be + * thought of the timeout time if no additional wait was added to a method. Increase if your system is slow. + */ + private final int PAUSE_LENGTH = 5; + + /** + * Private selectors from the navigation toolbar + */ + private final By createMenuDrp = By.cssSelector( + "#phenotips-globalTools > div > div > ul > li:nth-child(1) > span" + ); + + private final By newPatientLink = By.id("create-patient-record"); + + private final By browseMenuDrp = By.cssSelector( + "#phenotips-globalTools > div > div > ul > li:nth-child(2) > span"); + + private final By viewAllPatientsLink = By.cssSelector( + "#phenotips-globalTools > div > div > ul > li:nth-child(2) > ul > li:nth-child(1) > span > a"); + + private final By loadingStatusBar = By.id("patients-ajax-loader"); + + private final By inProgressMsg = By.cssSelector("div[class='xnotification xnotification-inprogress']"); + + /** + * Common URLs, specify address of the PC instance Possible mutation in BasePage ctor, get URLs from SystemProperty + */ + protected String HOMEPAGE_URL = "http://localhost:8083"; + + protected String EMAIL_UI_URL = "http://localhost:8085"; + + /** + * Declaration of the webdriver and the explicit waiting objects. Will be initialized when a test runs and any page + * class instantiated. See BasePage ctor. + */ + protected WebDriver superDriver; // Initialized only when a test suite runs and needs a page, so see BaseTest class. + + private WebDriverWait pause; + + private WebDriverWait longPause; // Use to wait for element to disappear. + + /** + * CTOR. The timeout period is defined here. We can also set the polling interval if need be. + * + * @param aDriver is the instance of webdriver created for the test. Must not be {@code null} + */ + public BasePage(WebDriver aDriver) + { + // Sets the HOMEPAGE_URL and EMAIL_UI_URL if they were passed in as command line + // property to JVM. Otherwise, will use what HOMEPAGE_URL and EMAIL_UI_URL as defined + // in its declaration at the top of this page. + final String homePageParameter = System.getProperty("homePageURL"); + final String emailUIPageParameter = System.getProperty("emailUIPageURL"); + + if (homePageParameter != null) { + HOMEPAGE_URL = homePageParameter; + // Debug message in BaseTest when static webDriver gets instantiated + } + + if (emailUIPageParameter != null) { + EMAIL_UI_URL = emailUIPageParameter; + // Debug message in BaseTest when static webDriver gets instantiated + } + + // Set the reference to the webdriver passed from a test case and initialize waiting times + // longPause is used to wait for something to disappear. Ex. loading bar, or a sending message. + // Might take longer than PAUSE_LENGTH seconds, so give it up to a minute. + superDriver = aDriver; + pause = new WebDriverWait(superDriver, PAUSE_LENGTH); + longPause = new WebDriverWait(superDriver, PAUSE_LENGTH + 55); + } + + /************************************* + * Generic page interaction methods + *************************************/ + /** + * Explicitly wait for the passed element to appear, upto the timeout specified in {@code pause} Checks immediately + * and then keeps polling at the default interval. + * + * @param elementSelector specifies the element selector on the page. Must not be {@code null} + * @Throws TimeOutException (implicit) from selenium if it fails to locate element within timeout + */ + public void waitForElementToBePresent(By elementSelector) + { + pause.until(ExpectedConditions.presenceOfElementLocated(elementSelector)); + } + + /** + * Explicitly wait for the passed element to disappear, upto the timeout specified in {@code pause} Checks + * immediately and then keeps polling at the default interval. Will return immediately if it cannot find element on + * that immediate first try. + * + * @param elementSelector specifies the element selector on the page. Must not be {@code null} + * @Throws TimeOutException (implicit) from selenium if it fails to locate element within timeout + */ + public void waitForElementToBeGone(By elementSelector) + { + longPause.until(ExpectedConditions.invisibilityOfElementLocated(elementSelector)); + } + + /** + * Explicitly wait for the specified element to be clickable. Useful for when a modal blocks the access of the rest + * of the page (i.e. waiting for the modal to close). + * + * @param elementSelector specifies the element to wait for clickable. Must not be {@code null} + */ + public void waitForElementToBeClickable(By elementSelector) + { + pause.until(ExpectedConditions.elementToBeClickable(elementSelector)); + } + + /** + * Determines if the passed element is clickable or not. + * + * @param elementSelector is the element to check for clickability + * @return a boolean stating whether it was clickable or not. true for clickable, and false for unclickable. + */ + public boolean isElementClickable(By elementSelector) + { + try { + waitForElementToBeClickable(elementSelector); + } catch (TimeoutException e) { + return false; // Could not find element, took too long + } + return true; + } + + /** + * Explicitly sleep for a full n seconds. Does not wait on anything specific. Useful when it is difficult to specify + * an element to wait and check upon. Ex. Filtering update + * + * @param n is time to pause in seconds. + * @Throws InterruptedException (implicit) if thread is interrupted, ex. SIGIGNT. Handles it by just continuing + * after printing a message. + */ + @Step("Unconditional wait (wait the full length) of {0} seconds.") + public void unconditionalWaitNs(long n) + { + try { + Thread.sleep(n * 1000); + } catch (InterruptedException e) { + System.err.println("Test was interrupted during an unconditional wait of " + n + " seconds!"); + } + } + + /** + * Waits for an element and then tries to click. The findElement() might have a (short) implicit wait but it is + * safer to explicitly wait for the element first. + * + * @param elementSelector is the element to click on. Should not be {@code null} + */ + public void clickOnElement(By elementSelector) + { + waitForElementToBePresent(elementSelector); + clickOnElement(superDriver.findElement(elementSelector)); + } + + /** + * Overloaded this function to allow for a located WebElement (instead of a selector) to be passed to it. This + * function catches the ElementNotInteractableException that can happen during a click which is due to the element + * being outside of Selenium's viewport. Strangely, sometimes we have to issue an explicit scroll using JS as + * Selenium fails to do so automatically in its built in .click() method. + * + * @param aWebElement the element to click on. It will scroll to this element if outside of viewport. + */ + public void clickOnElement(WebElement aWebElement) + { + try { + aWebElement.click(); + } catch (WebDriverException e) { + ((JavascriptExecutor) superDriver).executeScript("arguments[0].scrollIntoView();", aWebElement); + try { + aWebElement.click(); + } catch (WebDriverException f) { + ((JavascriptExecutor) superDriver).executeScript("arguments[0].click();", aWebElement); + System.out.println("Warning: Force click on element: " + aWebElement); + } + } + } + + /** + * Similar to {@link BasePage#clickOnElement(By)}. Just sends a specified input string to the element. This usually + * should be a text box. + * + * @param elementSelector is the selector. It must accept a string input from keyboard. + * @param input an arbitrary string to input. + * @Throws Some kind of Element-not-interactable exception (implicit) when element cannot accept keyboard string + */ + public void clickAndTypeOnElement(By elementSelector, String input) + { + clickOnElement(elementSelector); + superDriver.findElement(elementSelector).clear(); // Clear first + superDriver.findElement(elementSelector).sendKeys(input); + } + + /** + * Click on the element, usually a text box, and clear the contents. + * + * @param elementSelector The selector for the element to clear the contents of, usually a text box. + */ + public void clickAndClearElement(By elementSelector) + { + clickOnElement(elementSelector); + superDriver.findElement(elementSelector).clear(); // Clear first + } + + /** + * Toggles the specified checkbox to the enabled state. If it is already selected/enabled, it does not click on it + * again. + * + * @param checkboxSelector is the By selector for the checkbox to changed to enabled state. + */ + public void toggleCheckboxToChecked(By checkboxSelector) + { + waitForElementToBePresent(checkboxSelector); + if (!superDriver.findElement(checkboxSelector).isSelected()) { + clickOnElement(checkboxSelector); + } + } + + /** + * Returns a Bool after waiting and then checking if the element is present + * + * @param elementSelector is the element in question. Requires not {@code null} + * @return True for present and False for not present (or is taking too long to find/appear). + */ + public Boolean isElementPresent(By elementSelector) + { + try { + waitForElementToBePresent(elementSelector); + } catch (TimeoutException e) { + return false; // Could not find element, took too long + } + return true; + } + + /** + * Forces a scroll via JS so that the indicated element is in Selenium's viewport. Without this, sometimes a + * "ElementClickInterceptedException" exception is thrown as the element to click is out of window or otherwise + * blocked by some other element. + * + * Not sure why Selenium doesn't do this automatically for Firefox + * + * @param elementSelector a By selector to indicate which element you want to scroll into view. + */ + public void forceScrollToElement(By elementSelector) + { + waitForElementToBePresent(elementSelector); + + WebElement webElement = superDriver.findElement(elementSelector); + ((JavascriptExecutor) superDriver).executeScript("arguments[0].scrollIntoView();", webElement); + } + + /** + * Waits for an elements presence then forcefully clicks using JS on it. Sometimes, selectors have properties of + * hidden even thought they might not necessarialy be so. + * + * @param elementSelector a By selector indiciating the element to click on. + */ + public void forceClickOnElement(By elementSelector) + { + waitForElementToBePresent(elementSelector); + + WebElement webElement = superDriver.findElement(elementSelector); + ((JavascriptExecutor) superDriver).executeScript("arguments[0].click();", webElement); + } + + /** + * Provides a pre-order traversal of a tree structure, clicking on each node and building a List of Strings + * representing the text that Selenium can find at each node. + * + * @param rootPath is the By selector of where the root nodes of the tree structure is + * @param childrenPath within each root structure found, children under {$code rootPath} are searched for using this + * path. + * @param childrenLabelLocation is the path to where the label text is located for the children. + * @return a (might be empty) list of Strings representing the visits of the traversal. In case of unequal lengths + * of the lists (i.e. it found more buttons than labels), it prints error message to stdout then returns NULL + */ + public List preOrderTraverseAndClick(By rootPath, By childrenPath, By childrenLabelLocation) + { + waitForElementToBePresent(rootPath); // Must wait before any driver does a find + unconditionalWaitNs(2); // Wait two more seconds in case the expansion is actually still loading. + + List loLabels = new ArrayList<>(); + + // Finds all children, pre-order, no recursion needed? + List loButtons = superDriver + .findElements(new ByChained(rootPath, childrenPath)); + + List loButtonsLabels = superDriver + .findElements(new ByChained(rootPath, childrenLabelLocation)); + + Iterator buttonIter = loButtons.iterator(); + Iterator labelsIter = loButtonsLabels.iterator(); + + // Check that list of buttons size (clickable area) == size of list of labels (text strings) + if (loButtons.size() != loButtonsLabels.size()) { + System.out.println("Unequal array sizes for buttons and labels in preOrderTraverseAndClick: " + + "Found Buttons: " + loButtons.size() + " but found Labels: " + loLabels.size()); + return null; + } + + forceScrollToElement(rootPath); + + while (buttonIter.hasNext() && labelsIter.hasNext()) { + WebElement theButton = buttonIter.next(); + WebElement theLabel = labelsIter.next(); + + // Might need to force a scroll to a checklist item again + clickOnElement(theButton); + System.out.println("DEBUG: Clicking on: " + theLabel.getText()); + + loLabels.add(theLabel.getText()); + } + + return loLabels; + } + + /** + * Extracts the text strings for the elements that can be found via the labelsSelector. That selector can represent + * zero or more instances of that element on the page containing a text value. + * + * @param labelsSelector is the selector for zero or more elements existing on the page containing text to extract. + * @return a, possibly empty, list of Strings representing the text from each instance of the selector. A String + * will be the empty String ("") if an element had no text on it. + */ + public List getLabelsFromList(By labelsSelector) + { + waitForElementToBePresent(labelsSelector); + + List loTextStrings = new ArrayList<>(); + List loFoundLabels = superDriver.findElements(labelsSelector); + + for (WebElement e : loFoundLabels) { + loTextStrings.add(e.getText()); + } + + return loTextStrings; + } + + /********************************************************************** + * Common toolbar methods. These methods can be called from any page. + **********************************************************************/ + /** + * Logs out by clicking on the Log Out link on the navigation bar. + * + * @return a new LoginPage object. + * @requires: A user to already by logged in. + */ + @Step("Log out") + public LoginPage logOut() + { + clickOnElement(logOutLink); + return new LoginPage(superDriver); + } + + /** + * Navigates to the "Browse... -> Browse patients" page + * + * @return new instance of the browse patients page (AllPatientsPage) + * @requires: A user to already by logged in. + */ + @Step("Navigate to All Patients page") + public AllPatientsPage navigateToAllPatientsPage() + { + clickOnElement(browseMenuDrp); + + clickOnElement(viewAllPatientsLink); + + waitForLoadingBarToDisappear(); + + return new AllPatientsPage(superDriver); + } + + /** + * Navigates to the Admin Settings page by clicking the "Administrator" (gears icon) link at the top left of the + * navbar + * + * @return new object of the AdminSettingsPage + * @requires: An administrator to already be logged in. + */ + @Step("Navigate to Admin Settings Page") + public AdminSettingsPage navigateToAdminSettingsPage() + { + clickOnElement(adminLink); + return new AdminSettingsPage(superDriver); + } + + /** + * Navigates to MockMock's (fake SMTP service) email landing page. + * + * @return new object of the EmailUIPage where the email inbox can be seen + */ + @Step("Navigate to Email Inbox Page (Fake SMTP UI)") + public EmailUIPage navigateToEmailInboxPage() + { + superDriver.navigate().to(EMAIL_UI_URL); + return new EmailUIPage(superDriver); + } + + /** + * Navigates to the create patient page by "Create... -> New patient" + * + * @return a new object of the CreatePatientPage where the creation of a new patient is performed. + */ + @Step("Navigate to Create Patient Page (Create a new patient)") + public CreatePatientPage navigateToCreateANewPatientPage() + { + clickOnElement(createMenuDrp); + + clickOnElement(newPatientLink); + + return new CreatePatientPage(superDriver); + } + + /** + * Retrieves the approval pending message that a user receives when they are unapproved and waiting to be granted + * access. It should be "Please wait for your account to be approved. Thank you." + * + * @return A String representing the approval pending message. + */ + public String getApprovalPendingMessage() + { + waitForElementToBePresent(approvalPendingMessage); + return superDriver.findElement(approvalPendingMessage).getText(); + } + + /** + * Navigates to the homepage by clicking on the PhenomeCentral Logo at the top left corner of the top toolbar. Note + * that this gets overridden in the EmailUIPage class because the PC logo doesn't appear there and we have to + * explicitly navigate back to the homepage by explicitly navigating to the HOMEPAGE_URL + * + * @return A new instance of a HomePage as we navigate there. + */ + @Step("Navigate to PC home page for this instance") + public HomePage navigateToHomePage() + { + clickOnElement(phenomeCentralLogoBtn); + return new HomePage(superDriver); + } + + /** + * Explicitly wait for the loading bar to disappear. This is the xwiki loading bar which appears wherever there is a + * live table filtration. Examples include the patient list (AllPatientsPage) and the match table + * (AdminMatchNotificationPage). + */ + public void waitForLoadingBarToDisappear() + { + waitForElementToBeGone(loadingStatusBar); + } + + /** + * Explicitly wait until the spinning message (xwiki generated) that appears on the bottom of the page disappears. + * This message appears when sending a request to the servers. It has several different texts as they appear in + * multiple areas but they usually have a black background before changing to green. Example, deleting a patient + * makes the black "Sending Request..." before changing to green "Done!". Also, importing JSON patient also makes + * "Importing JSON, please be patient..." followed by "Done!". + */ + public void waitForInProgressMsgToDisappear() + { + waitForElementToBePresent(inProgressMsg); + waitForElementToBeGone(inProgressMsg); + } + + /** + * Dismiss the "You have unsaved changes on this page. Continue?" warning message that appears when trying to + * navigate away from a page with unsaved edits. It selects "Leave" button. This is a native browser (operating + * system, not web-based) warning dialogue. Requires: That there actually be a warning box open and active on the + * browser. Selenium will throw a "NoAlertPresentException" if there is actually no dialogue to interact with. + */ + @Step("Dismiss the unsaved changes warning dialogue") + public void dismissUnsavedChangesWarning() + { + superDriver.switchTo().alert().accept(); + superDriver.switchTo().defaultContent(); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/CommonInfoSelectors.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/CommonInfoSelectors.java new file mode 100644 index 00000000..a38e9c07 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/CommonInfoSelectors.java @@ -0,0 +1,189 @@ +package org.phenotips.endtoendtests.pageobjects; + +import org.phenotips.endtoendtests.common.CommonInfoEnums; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.Select; + +import io.qameta.allure.Step; + +/** + * Contains common selectors for the accordion sections on the create and view patient info pages. Ex. + * http://localhost:8083/data/P0000015 and http://localhost:8083/edit/data/P0000015 + */ +public abstract class CommonInfoSelectors extends BasePage implements CommonInfoEnums +{ + private final By modifyPermissionsBtn = By.cssSelector("span[title=\"Modify visibility and collaborations\"]"); + + private final By privateRadioBtn = By.cssSelector("input[type=radio][name=visibility][value=private]"); + + private final By matchableRadioBtn = By.cssSelector("input[type=radio][name=visibility][value=matchable]"); + + private final By publicRadioBtn = By.cssSelector("input[type=radio][name=visibility][value=public]"); + + private final By newCollaboratorBox = By.id("new-collaborator-input"); + + private final By firstCollaboratorResult = By.cssSelector("div.suggestItem > div.user > div.user-name"); + + private final By updateConfirmBtn = By.cssSelector("input.button[value=Update]"); + + private final By privilageLevelDrps = By.cssSelector("select[name=accessLevel]"); + + private final By deleteCollaboratorBtn = By.cssSelector("span[title=\"Remove this collaborator\"]"); + + // Selectors for the sections below + private final By patientInfoSection = By.id("HPatientinformation"); // "Patient information" + + private final By familyHistorySection = By.id("HFamilyhistoryandpedigree"); // "Family history and pedigree" + + private final By prenatalHistorySection = By.id("HPrenatalandperinatalhistory"); // Prenatal and perinatal history + + private final By medicalHistorySection = By.id("HMedicalhistory"); // Medical history + + private final By measurementsSection = By.id("HMeasurements"); // Measurements + + private final By clinicalSymptomsSection = By.id("HClinicalsymptomsandphysicalfindings"); + // Clinical symptoms and physical findings + + private final By suggestedGenesSection = By.id("HSuggestedGenes"); // Suggested Genes + + private final By genotypeInfoSection = By.id("HGenotypeinformation"); // Genotype information + + private final By diagnosisSection = By.id("HDiagnosis"); // Diagnosis + + private final By similarCasesSection = By.id("HSimilarcases"); // Similar cases + + protected Map sectionMap = new HashMap(); + + /** + * CTOR. Initializes the map from an enum value to a specific element for the section + * + * @param aDriver is not {@code null} + */ + public CommonInfoSelectors(WebDriver aDriver) + { + super(aDriver); + sectionMap.put(SECTIONS.ClinicalSymptomsSection, clinicalSymptomsSection); + sectionMap.put(SECTIONS.DiagnosisSection, diagnosisSection); + sectionMap.put(SECTIONS.FamilyHistorySection, familyHistorySection); + sectionMap.put(SECTIONS.GenotypeInfoSection, genotypeInfoSection); + sectionMap.put(SECTIONS.MeasurementSection, measurementsSection); + sectionMap.put(SECTIONS.MedicalHistorySection, medicalHistorySection); + sectionMap.put(SECTIONS.PatientInfoSection, patientInfoSection); + sectionMap.put(SECTIONS.PrenatalHistorySection, prenatalHistorySection); + sectionMap.put(SECTIONS.SuggestedGenesSection, suggestedGenesSection); + sectionMap.put(SECTIONS.SimilarCasesSection, similarCasesSection); + } + + /** + * Iterates over each section in loSections (specified by enum values) and ensure that they are present on the page. + * Only checks the title is present, not the actual contents. + * + * @param loSections a possibly empty array of sections specified by the SECTIONS enum + * @return True if all the sections in loSections are visible and false if one of them isn't. + */ + public Boolean checkForVisibleSections(SECTIONS[] loSections) + { + for (SECTIONS aSection : loSections) { + Boolean presence = isElementPresent(sectionMap.get(aSection)); + System.out.println("CommonInfoSelectors Line 44"); + if (!presence) { + return false; + } + } + return true; + } + + /** + * Sets the global visibility of the patient. Opens the access rights dialogue modal ("Modify Permissions") Defaults + * to private on invalid input. + * + * @param theVisibility is string, one of "private", "matchable", "public". Must be exact. + */ + @Step("Set patient's global visibility to: {0}") + public void setGlobalVisibility(String theVisibility) + { + clickOnElement(modifyPermissionsBtn); + waitForElementToBePresent(privateRadioBtn); + switch (theVisibility) { + case "private": + clickOnElement(privateRadioBtn); + break; + case "matchable": + clickOnElement(matchableRadioBtn); + break; + case "public": + clickOnElement(publicRadioBtn); + break; + default: + clickOnElement(privateRadioBtn); + break; + } + clickOnElement(updateConfirmBtn); + waitForElementToBeClickable(logOutLink); + unconditionalWaitNs( + 2); // This might be needed still. For some reason, modal does not close immediately and nothing to wait for. + } + + /** + * Adds a collaborator to the patient via the "Modify Permissions" modal. Searches for name and clicks on first + * result, therefore, assumes that there is at least one result. Sets the privilage to the one specified. + * + * @param collaboratorName is the exact name of the collaborator to add. + * @param privilageLevel is one of three levels of privilages specified by the PRIVIALGE enum. + */ + @Step("Add collaborator to patient with: Collaborator name: {0} and Privilage level of: {1}") + public void addCollaboratorToPatient(String collaboratorName, PRIVILAGE privilageLevel) + { + clickOnElement(modifyPermissionsBtn); + clickAndTypeOnElement(newCollaboratorBox, collaboratorName); + clickOnElement(firstCollaboratorResult); + superDriver.findElement(newCollaboratorBox).sendKeys(Keys.ENTER); // Looks like we'll have to press enter + + List loPrivilageDropdowns = superDriver.findElements(privilageLevelDrps); + Select bottomMostPDrop = new Select(loPrivilageDropdowns.get(loPrivilageDropdowns.size() - 1)); + + switch (privilageLevel) { + case CanView: + bottomMostPDrop.selectByVisibleText("Can view the record"); + break; + case CanViewAndModify: + bottomMostPDrop.selectByVisibleText("Can view and modify the record"); + break; + case CanViewAndModifyAndManageRights: + bottomMostPDrop.selectByVisibleText("Can view and modify the record and manage access rights"); + break; + } + + forceClickOnElement(updateConfirmBtn); + waitForElementToBeClickable(logOutLink); + unconditionalWaitNs(2); // Modal does not seem to be fully closed even when logOut link turns to clickable? + } + + /** + * Deletes the Nth collaborator from the permissions modal. + * + * @param n is the Nth collaborator (n >= 1) Must supply valid Nth collaborator otherwise array out of bounds + * exception will be thrown. + */ + @Step("Remove the {0}th collaborator from the list") + public void removeNthCollaborator(int n) + { + clickOnElement(modifyPermissionsBtn); + waitForElementToBePresent(newCollaboratorBox); + + List loDeleteCollaboratorBtns = superDriver.findElements(deleteCollaboratorBtn); + loDeleteCollaboratorBtns.get(n - 1).click(); + + forceClickOnElement(updateConfirmBtn); + waitForElementToBeClickable(logOutLink); + unconditionalWaitNs(2); // Likewise, the modal does not seem to be fully closed even when logOut is clickable? + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/CommonSignUpSelectors.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/CommonSignUpSelectors.java new file mode 100644 index 00000000..3e146994 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/CommonSignUpSelectors.java @@ -0,0 +1,28 @@ +package org.phenotips.endtoendtests.pageobjects; + +import org.openqa.selenium.By; + +/** + * This interface contains selectors that are used on a sign up page. These appear on both the sign up page that a user + * sees and when an Admin tries to add a user manually. i.e. http://localhost:8083/register/PhenomeCentral/WebHome and + * http://localhost:8083/admin/XWiki/XWikiPreferences?editor=globaladmin§ion=Users# + */ +public interface CommonSignUpSelectors +{ + By newUserBtn = By.id("addNewUser"); + By firstNameBox = By.id("register_first_name"); + By lastNameBox = By.id("register_last_name"); + By userNameBox = By.id("xwikiname"); + By passwordBox = By.id("register_password"); + By confirmPasswordBox = By.id("register2_password"); + By emailBox = By.id("register_email"); + By affiliationBox = By.id("register_affiliation"); + By referralBox = By.id("register_referral"); // "How did you hear about / Who referred you to PhenomeCentral?" + By reasoningBox = By.id("register_comment"); // Why are you requesting access to PhenomeCentral? + // Checkboxes + By professionalCheckbox = By.id("confirmation_clinician"); + By liabilityCheckbox = By.id("confirmation_research"); + By nonIdentificationCheckbox = By.id("confirmation_identify"); + By cooperationCheckbox = By.id("confirmation_publication"); + By acknoledgementCheckbox = By.id("confirmation_funding"); +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/CreatePatientPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/CreatePatientPage.java new file mode 100644 index 00000000..129efd0e --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/CreatePatientPage.java @@ -0,0 +1,1356 @@ +package org.phenotips.endtoendtests.pageobjects; + +import org.phenotips.endtoendtests.common.CommonPatientMeasurement; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.Select; + +import io.qameta.allure.Step; + +/** + * Represents the page reached when "Create... -> New patient" is clicked on the navbar Ex. + * http://localhost:8083/edit/data/Pxxxxxxx (new patient ID) + */ +public class CreatePatientPage extends CommonInfoSelectors +{ + /************************************************************ + * "Patient Consent" (Consents Granted) Section - Selectors + ************************************************************/ + private final By realPatientConsentBox = By.id("real-consent-checkbox"); + + private final By geneticConsentBox = By.id("genetic-consent-checkbox"); + + private final By shareHistoryConsentBox = By.id("share_history-consent-checkbox"); + + private final By shareImagesConsentBox = By.id("share_images-consent-checkbox"); + + private final By matchingConsentBox = By.id("matching-consent-checkbox"); + + private final By patientConsentUpdateBtn = By.cssSelector("#patient-consent-update > a:nth-child(1)"); + + /********************************************* + * "Patient Information" Section - Selectors + *********************************************/ + private final By identifierBox = By.id("PhenoTips.PatientClass_0_external_id"); + + private final By lifeStatusDrp = By.id("PhenoTips.PatientClass_0_life_status"); + + private final By dobMonthDrp = + By.cssSelector("#date-of-birth-block > div > div:nth-child(2) > div > div > span > select.month"); + + private final By dobYearDrp = + By.cssSelector("#date-of-birth-block > div > div:nth-child(2) > div > div > span > select.year"); + + private final By doDeathMonthDrp = + By.cssSelector("#date-of-death-block > div > div:nth-child(2) > div > div > span > select.month"); + + private final By doDeathYearDrp = + By.cssSelector("#date-of-death-block > div > div:nth-child(2) > div > div > span > select.year"); + + private final By maleGenderBtn = By.id("xwiki-form-gender-0-0"); + + private final By femaleGenderBtn = By.id("xwiki-form-gender-0-1"); + + private final By otherGenderBtn = By.id("xwiki-form-gender-0-2"); + + private final By unknownGenderBtn = By.id("xwiki-form-gender-0-3"); + + private final By congenitalOnsentBtn = By.id("PhenoTips.PatientClass_0_global_age_of_onset_HP:0003577"); + + private final By ageOfOnsetBtns = By.cssSelector("div.global_age_of_onset > div > div > ul > li.term-entry"); + + private final By modeOfInheritanceChkboxes = + By.cssSelector("div.global_mode_of_inheritance > div > div > ul > li.term-entry"); + + private final By indicationForReferralBox = By.id("PhenoTips.PatientClass_0_indication_for_referral"); + + /******************************************************* + * "Family history and pedigree" Section - Selectors + *******************************************************/ + private final By editPedigreeBox = By.cssSelector("div.pedigree-wrapper > div"); + + private final By editPedigreeOKBtn = By.cssSelector("input.button[name=ok]"); + + private final By assignFamilyRadioBtn = By.id("pedigreeInputAssignFamily"); + + private final By familySearchInputBox = By.id("family-search-input"); + + private final By firstFamilySuggestion = By.cssSelector("span.suggestValue"); + + private final By paternalEthnicityBox = By.id("PhenoTips.PatientClass_0_paternal_ethnicity_2"); + + private final By maternalEthnicityBox = By.id("PhenoTips.PatientClass_0_maternal_ethnicity_2"); + + private final By addEthnicityBtns = By.cssSelector("div.family-info a[title=add]"); + + private final By healthConditionsFoundInFamily = By.id("PhenoTips.PatientClass_0_family_history"); + + /******************************************************* + * "Prenatal and perinatal history" Section - Selectors + *******************************************************/ + private final By termBirthCheckbox = By.id("PhenoTips.PatientClass_0_gestation_term"); + + private final By gestationWeeksBox = + By.cssSelector("input[type=text][name=\"PhenoTips.PatientClass_0_gestation\"]"); + + private final By maternalAgeBox = By.id("PhenoTips.ParentalInformationClass_0_maternal_age"); + + private final By paternalAgeBox = By.id("PhenoTips.ParentalInformationClass_0_paternal_age"); + + private final By APGARScore1MinDrp = By.id("PhenoTips.PatientClass_0_apgar1"); + + private final By APGARScore5MinDrp = By.id("PhenoTips.PatientClass_0_apgar5"); + + private final By otherPregnancyPhenotypeBox = By.xpath( + "//*[@id='HPregnancy-history']/parent::*/div[@class='prenatal_phenotype-other custom-entries']/input[@type='text']"); + + private final By otherDevelopmentPhenotypeBox = By.xpath( + "//*[@id='HPrenatal-development']/parent::*/div[@class='prenatal_phenotype-other custom-entries']/input[@type='text']"); + + private final By otherDeliveryPhenotypeBox = By.xpath( + "//*[@id='HDelivery']/parent::*/div[@class='prenatal_phenotype-other custom-entries']/input[@type='text']"); + + private final By otherGrowthPhenotypeBox = By.xpath( + "//*[@id='HNeonatal-growth-parameters']/parent::*/div[@class='prenatal_phenotype-other custom-entries']/input[@type='text']"); + + private final By otherComplicationsPhenotypeBox = By.xpath( + "//*[@id='HPerinatal-complications']/parent::*/div[@class='prenatal_phenotype-other custom-entries']/input[@type='text']"); + + private final By prenatalNotesBox = By.id("PhenoTips.PatientClass_0_prenatal_development"); + + /******************************************************* + * "Measurements" Section - Selectors + *******************************************************/ + private final By addMeasurementBtn = By.cssSelector("div.measurement-info a.add-data-button"); + + private final By measurementYearDrp = By.cssSelector("div.calendar_date_select select.year"); + + private final By measurementMonthDrp = By.cssSelector("div.calendar_date_select select.month"); + + private final By todayCalendarLink = By.linkText("Today"); + + private final By measurementDateBoxes = By.id("PhenoTips.MeasurementsClass_0_date"); + + private final By weightBox = By.id("PhenoTips.MeasurementsClass_0_weight"); + + private final By heightBox = By.id("PhenoTips.MeasurementsClass_0_height"); + + private final By armSpanBox = By.id("PhenoTips.MeasurementsClass_0_armspan"); + + private final By sittingHeightBox = By.id("PhenoTips.MeasurementsClass_0_sitting"); + + private final By headCircumferenceBox = By.id("PhenoTips.MeasurementsClass_0_hc"); + + private final By philtrumLengthBox = By.id("PhenoTips.MeasurementsClass_0_philtrum"); + + private final By leftEarLengthBox = By.id("PhenoTips.MeasurementsClass_0_ear"); + + private final By rightEarLengthBox = By.id("PhenoTips.MeasurementsClass_0_ear_right"); + + private final By outherCanthalDistanceBox = By.id("PhenoTips.MeasurementsClass_0_ocd"); + + private final By innterCanthalDistanceBox = By.id("PhenoTips.MeasurementsClass_0_icd"); + + private final By palpebralFissureLengthBox = By.id("PhenoTips.MeasurementsClass_0_pfl"); + + private final By interpupilaryDistanceBox = By.id("PhenoTips.MeasurementsClass_0_ipd"); + + private final By leftHandLengthBox = By.id("PhenoTips.MeasurementsClass_0_hand"); + + private final By leftPalmLengthBox = By.id("PhenoTips.MeasurementsClass_0_palm"); + + private final By leftFootLengthBox = By.id("PhenoTips.MeasurementsClass_0_foot"); + + private final By rightHandLengthBox = By.id("PhenoTips.MeasurementsClass_0_hand_right"); + + private final By rightPalmLengthBox = By.id("PhenoTips.MeasurementsClass_0_palm_right"); + + private final By rightFootLengthBox = By.id("PhenoTips.MeasurementsClass_0_foot_right"); + + /****************************************************************************** + * "Clinical symptoms and physical findings" (Phenotypes) Section - Selectors + ******************************************************************************/ + private final By phenotypeSearchBox = By.id("quick-phenotype-search"); + + // Xwiki suggestion dialogue is overlayed ontop of entire page document rather than underneath + // the phenotype section div. Hence the vague selector. + private final By firstPhenotypeSuggestion = By.cssSelector("li.xitem > div"); + + private final By addPhenotypeDetailsBtns = By.cssSelector("button.add"); + + private final By editPhenotypeDetailsBtns = By.cssSelector("button.edit"); + + private final By expandCaretBtns = By.cssSelector( + "div.phenotype-details.focused span.collapse-button, div.phenotype-details.focused span.expand-tool"); + + private final By phenotypeDetailsLabels = By.cssSelector("div.phenotype-details.focused label"); + + private final By phenotypesSelectedLabels = By.cssSelector("div.summary-item > label.yes"); + + // Different selectors for when the thunderbolt symbol is present or not. Lightning appears when auto added + // by measurement information. + private final By phenotypesAutoSelectedByMeasurementLabels = + By.xpath("//div[@class='summary-item' and span[@class='fa fa-bolt']]/label[@class='yes']"); + + private final By phenotypesManuallySelectedLabels = + By.xpath("//div[@class='summary-item' and not(span[@class='fa fa-bolt'])]/label[@class='yes']"); + + /******************************************************* + * "Genotype information" Section - Selectors + *******************************************************/ + private final By addGeneBtn = By.cssSelector("a[title*='Add gene']"); + + private final By geneNameBoxes = By.cssSelector( + "#extradata-list-PhenoTips\\.GeneClass_PhenoTips\\.GeneVariantClass > tbody > tr > td:nth-child(2) > input[type=text]"); + + private final By geneStatusDrps = By.cssSelector("td.Status > select"); + + private final By geneStrategySequencingCheckboxes = By.cssSelector( + "td.Strategy > label > input[value=sequencing]"); + + private final By geneStrategyDeletionCheckboxes = By.cssSelector( + "td.Strategy > label > input[value=deletion]"); + + private final By geneStrategyFamilialMutationCheckboxes = By.cssSelector( + "td.Strategy > label > input[value=familial_mutation]"); + + private final By geneStrategyCommonMutationCheckboxes = By.cssSelector( + "td.Strategy > label > input[value=common_mutations]"); + + private final By firstGeneSuggestion = By.cssSelector("div.suggestItem > div > span.suggestValue"); + // First suggestion result for prenatal phenotypes too + + /******************************************************* + * "Diagnosis" Section - Selectors + *******************************************************/ + private final By clinicalDiagnosisBox = By.id("PhenoTips.PatientClass_0_clinical_diagnosis"); + + private final By clinicalDiagnosisCheckboxes = + By.cssSelector("input[id*='PhenoTips.PatientClass_0_clinical_diagnosis_ORDO:']"); + + private final By finalDiagnosisBox = By.id("PhenoTips.PatientClass_0_omim_id"); + + private final By finalDiagnosisCheckboxes = By.cssSelector("input[id*='PhenoTips.PatientClass_0_omim_id_']"); + + private final By additionalCommentsBox = By.id("PhenoTips.PatientClass_0_diagnosis_notes"); + + private final By caseSolvedCheckbox = By.id("PhenoTips.PatientClass_0_solved"); + + private final By pubMDIDBoxes = By.cssSelector("[id*='PhenoTips.PatientClass_0_solved__pubmed_id_']"); + + private final By addPubMDLink = By.cssSelector("div.diagnosis-info a[title=add]"); + + private final By deletePubMDBtns = By.cssSelector("div.diagnosis-info a[title='delete']"); + + private final By pubMDArticle = By.cssSelector("div.article-info"); + + private final By pubMDIDCheckStatus = + By.cssSelector("div.solved__pubmed_id > div.solved__pubmed_id > div > ol > li > div"); + + private final By resolutionNotesBox = By.id("PhenoTips.PatientClass_0_solved__notes"); + + /************************************************** + * Common Tree Traversal Selectors and Strings + **************************************************/ + // For tree traversals + private final By ageOfOnsetAndModeInheritanceChildBtn = By.cssSelector("ul > li.term-entry > input"); + + private final By ageOfOnsetAndModeInheritanceChildLabels = By.cssSelector("ul > li.term-entry > label"); + + private final String yesBtnSelectorString = "div.displayed-value > span.yes-no-picker > label.yes"; + + private final String labelSelectorString = "div.displayed-value > label.yes-no-picker-label"; + + private final By expandToolSpan = By.cssSelector("span[class=expand-tool]"); + + // Common Selectors for page + + private final By saveAndViewSummaryBtn = By.cssSelector("div.bottombuttons input[value='Save and view summary']"); + + private final By quickSaveBtn = By.cssSelector("div.bottombuttons input[value='Quick save']"); + + private final By cancelChangesSinceSaveBtn = + By.cssSelector("div.bottombuttons input[value='Cancel changes since last save']"); + + public CreatePatientPage(WebDriver aDriver) + { + super(aDriver); + } + + /**************************** + * Common Methods + ****************************/ + /** + * Hits the "Save and View Summary" button on the bottom left. + * + * @return navigating to the view page containing patient's full details so a new object of that type + */ + @Step("Save and View Summary of patient form") + public ViewPatientPage saveAndViewSummary() + { + clickOnElement(saveAndViewSummaryBtn); + return new ViewPatientPage(superDriver); + } + + /** + * Hits the "Quick Save" button on the bottom left. + * + * @return stay on the same page so return the same object + */ + @Step("Quick Save of patient form") + public CreatePatientPage quickSave() + { + clickOnElement(quickSaveBtn); + return this; + } + + /** + * Hits the "Cancel changes since last save" button on the bottom right. + * + * @return navigating to the view page containing patient's full details so a new object of that type + */ + @Step("Cancel changes on patient form") + public ViewPatientPage cancelChanges() + { + clickOnElement(cancelChangesSinceSaveBtn); + return new ViewPatientPage(superDriver); + } + + /**************************** + * Consent Methods + ****************************/ + /** + * Toggles the nth consent checkbox in the "Consents granted" section + * + * @param n which is an integer between 1-5 representing the specified checkbox. + * @return the same object as we are on the same page + */ + @Step("Toggle the {0}th consent box") + public CreatePatientPage toggleNthConsentBox(int n) + { + switch (n) { + case 1: + clickOnElement(realPatientConsentBox); + break; + case 2: + clickOnElement(geneticConsentBox); + break; + case 3: + clickOnElement(shareHistoryConsentBox); + break; + case 4: + clickOnElement(shareImagesConsentBox); + break; + case 5: + clickOnElement(matchingConsentBox); + break; + default: + System.out.println("Invalid nth consent box specified: " + n); + break; + } + return this; + } + + /** + * Helper method to toggle the first four consent boxes. These boxes are unchecked when first creating a patient + * (Real patient, Genetic Sequencing data consent, Medical/Family History consent, Medical Images/Photos consent) + * + * @return Stay on the same page, so return the same object. + */ + public CreatePatientPage toggleFirstFourConsentBoxes() + { + toggleNthConsentBox(1); + toggleNthConsentBox(2); + toggleNthConsentBox(3); + toggleNthConsentBox(4); + return this; + } + + /** + * Clicks on the "Update" button under the "Consents granted" section. Waits 5 seconds for consent to update. + * + * @return same object as we stay on the same page + */ + @Step("Click on 'Update' button for consent") + public CreatePatientPage updateConsent() + { + clickOnElement(patientConsentUpdateBtn); + unconditionalWaitNs(5); // No element to wait on as the state of the consents might not have changed. + return this; + } + + /******************************************** + * "Patient Information" Section - Methods + ********************************************/ + /** + * Clears and then sets the patient identifer field box. + * + * @param identifer the string that should be entered into the "Identifer" field under Patient Information + * @return stay on the same page so return the same instance of object + */ + @Step("Set the patient's identifier to: {0}") + public CreatePatientPage setIdentifer(String identifer) + { + clickOnElement(identifierBox); + superDriver.findElement(identifierBox).clear(); + clickAndTypeOnElement(identifierBox, identifer); + unconditionalWaitNs(1); // Gives "identifier already exists" if we navigate away too fast. + return this; + } + + /** + * Sets the Life Status dropdown of the patient. + * + * @param status is either "Alive" or "Deceased". Must be exact string otherwise Selenium throws + * NoSuchElementException exception. + * @return stay on the same page so return same object. + */ + @Step("Set patient's life status to: {0}") + public CreatePatientPage setLifeStatus(String status) + { + waitForElementToBePresent(lifeStatusDrp); + Select statusDrp = new Select(superDriver.findElement(lifeStatusDrp)); + + statusDrp.selectByVisibleText(status); + + return this; + } + + /** + * Sets the Date of Birth of the patient under Patient Information. In case of invalid params or unable to find + * selection, Selenium throws NoSuchElementException exception. + * + * @param month the Month as a String (01 - 12). Must exactly match the dropdown. + * @param year the year as a String (1500s - 2019). Must exactly match the dropdown. + * @return stay on the same page so return same object. + */ + @Step("Set the DOB for the patient to: {0} month and {1} year") + public CreatePatientPage setDOB(String month, String year) + { + Select monthDrp; + Select yearDrp; + + waitForElementToBePresent(dobMonthDrp); + monthDrp = new Select(superDriver.findElement(dobMonthDrp)); + yearDrp = new Select(superDriver.findElement(dobYearDrp)); + + monthDrp.selectByVisibleText(month); + yearDrp.selectByVisibleText(year); + + return this; + } + + /** + * Sets the Date of Death of the patient under Patient Information. In case of invalid params or unable to find + * selection, Selenium throws NoSuchElementException exception. Requires: Life Status set to "Deceased" so that Date + * of Death dropdowns are visible. + * + * @param month the Month as a String (01 - 12). Must exactly match the dropdown. + * @param year the year as a String (1500s - 2019). Must exactly match the dropdown. + * @return stay on the same page so return same object. + */ + @Step("Set the date of death for the patient to: {0} month and {1} year") + public CreatePatientPage setDateOfDeath(String month, String year) + { + Select monthDrp; + Select yearDrp; + + waitForElementToBePresent(doDeathMonthDrp); + monthDrp = new Select(superDriver.findElement(doDeathMonthDrp)); + yearDrp = new Select(superDriver.findElement(doDeathYearDrp)); + + monthDrp.selectByVisibleText(month); + yearDrp.selectByVisibleText(year); + + return this; + } + + /** + * Toggles the expansion of the given section. Forcibly scrolls elements into view using JS. For some reason, + * selenium doesn't do this. + * + * @param theSection is the section from the enum that we want to expand + * @return stay on the same page so the same object + */ + public CreatePatientPage expandSection(SECTIONS theSection) + { + forceScrollToElement(sectionMap.get(theSection)); + + clickOnElement(sectionMap.get(theSection)); + return this; + } + + /** + * Sets the patient's gender. Defaults to Unknown. Assumes that Patient Information section is expanded (selectors + * in that section visible). + * + * @param theGender is String representing gender radio button. Must be exact text. + * @return the same page so the same object. + */ + @Step("Set the gender of the patient to: {0}") + public CreatePatientPage setGender(String theGender) + { + waitForElementToBePresent(maleGenderBtn); + switch (theGender) { + case "Male": + clickOnElement(maleGenderBtn); + break; + case "Female": + clickOnElement(femaleGenderBtn); + break; + case "Other": + clickOnElement(otherGenderBtn); + break; + case "Unknown": + clickOnElement(unknownGenderBtn); + break; + default: + System.out.println("Invalid gender selected! Default to Unknown"); + break; + } + return this; + } + + /** + * Sets the Age of Onset under patient information. Currently, only works for congential onset. Assumes that Patient + * Information section is expanded (selectors in that section visible). + * + * @param theOnset onset specified by radio button text, must match exactly. + * @return Stay on the same page, return same object. + */ + @Step("Set the Age of Onset for the patient to: {0}") + public CreatePatientPage setOnset(String theOnset) + { + waitForElementToBePresent(congenitalOnsentBtn); + + switch (theOnset) { + default: + clickOnElement(congenitalOnsentBtn); + break; + } + return this; + } + + /** + * Traverses through all the options for the age of onset buttons, clicks on each one. + * + * @return a List of Strings which represent the Age of Onset radio button labels in a 'pre-order' traversal. + */ + @Step("Traverse through UI for Age of Onset") + public List cycleThroughAgeOfOnset() + { + + List loLabels = + preOrderTraverseAndClick(ageOfOnsetBtns, + ageOfOnsetAndModeInheritanceChildBtn, + ageOfOnsetAndModeInheritanceChildLabels); + + clickOnElement(congenitalOnsentBtn); + + return loLabels; + } + + /** + * Traverses through all the options for the mode of inheritance checkboxes. + * + * @return a List of Strings which represent the Mode Of Inheritance checkbox labels in a 'pre-order' traversal. + */ + @Step("Traverse through UI for the Mode of Inheritance") + public List cycleThroughModeOfInheritance() + { + + return preOrderTraverseAndClick(modeOfInheritanceChkboxes, + ageOfOnsetAndModeInheritanceChildBtn, + ageOfOnsetAndModeInheritanceChildLabels); + } + + /** + * Sets the "Indication for Referral box" in the Patient Information section. Currently, it does not clear the + * contents of the box, just concatenates to whatever is there. + * + * @param neededText is the String to enter into the input box. + * @return stay on the same page so return the same object. + */ + @Step("Set the indication for referral to: {0}") + public CreatePatientPage setIndicationForReferral(String neededText) + { + clickAndTypeOnElement(indicationForReferralBox, neededText); + return this; + } + + /*************************************************** + * "Family history and pedigree" Section - Methods + ***************************************************/ + /** + * Navigates to the Pedigree Editor page by clicking "OK" on the "Assign patient to family" modal. Assumes that the + * "Create a new family" radio option is selected by default. Requires the "Family History" section to be expanded + * + * @param familyName is the family name to base the pedigree off of. Pass "" (empty string) to specify the "Create a + * new family" radio option. Otherwise, this must be a valid existing family name. + * @return we navigate to the Pedigree Editor page so return new instance of that. + */ + @Step("Navigate to the pedigree editor (from patient form) with family name: {0}") + public PedigreeEditorPage navigateToPedigreeEditor(String familyName) + { + clickOnElement(editPedigreeBox); + + // Case where we want to specify a family, also need to ensure that the dialogue is actually there. + if (!familyName.equals("") && isElementPresent(editPedigreeOKBtn)) { + clickOnElement(assignFamilyRadioBtn); + clickAndTypeOnElement(familySearchInputBox, familyName); + clickOnElement(firstFamilySuggestion); + } + + // If we are editing a pedigree, there is no family selection dialogue that appears, hence just + // check for an OK button before trying to click. + if (isElementPresent(editPedigreeOKBtn)) { + clickOnElement(editPedigreeOKBtn); + } + + return new PedigreeEditorPage(superDriver); + } + + /** + * Traverses through the options for the health conditions found in family yes/no boxes. + * + * @return a List of Strings which represent the health conditions found under "Family history and pedigree" + */ + @Step("Traverse through the familial health conditions UI") + public List cycleThroughFamilialHealthConditions() + { + + return preOrderTraverseAndClick(By.cssSelector("div.family-info > div.fieldset"), + By.cssSelector(yesBtnSelectorString), + By.cssSelector(labelSelectorString)); + } + + /** + * Sets the ethnicity of the patient in the "Family History and Pedigree" section. Defaults to Maternal ethnicity in + * case of invalid maternity passed. Will select the first option in the suggestions. + * + * @param maternity pass either "Paternal" or "Maternal" + * @param ethnicity is the ethncity to set. Requires this to be as close as possible to an exact match to + * suggestions dropdown. + * @return Stay on the same page so return the same object. + */ + @Step("Set the {0} ethnicity to {1}") + public CreatePatientPage setEthnicity(String maternity, String ethnicity) + { + if (maternity.equals("Paternal")) { + clickAndTypeOnElement(paternalEthnicityBox, ethnicity); + clickOnElement(firstFamilySuggestion); + } else { + clickAndTypeOnElement(maternalEthnicityBox, ethnicity); + clickOnElement(firstFamilySuggestion); + } + return this; + } + + /** + * Inputs a note into the Health Conditions within the "Family History and pedigree" section. + * + * @param note to type into the box. Any string. Will concatenate to what is there already. + * @return Stay on the same page so return the same object. + */ + @Step("Set the Health Conditions in Family note box to: {0}") + public CreatePatientPage setHealthConditionsFoundInFamily(String note) + { + clickAndTypeOnElement(healthConditionsFoundInFamily, note); + return this; + } + + /***************************************************** + * "Prenatal and perinatal history" Section - Methods + *****************************************************/ + /** + * Traverses through the options for the health conditions found in Prenatal and perinatal history yes/no boxes. + * Requires: The "Prenatal and perinatal history" section to be expanded and that none of the yes/no options are + * already selected/expanded (i.e. should be at the state of a new patient) Otherwise, traversal result might be off + * due to presence of additional (appearing) selectors. + * + * @return a List of Strings which represent the health conditions found under the yes/no boxes of "Prenatal and + * perinatal history" + */ + @Step("Set the DOB for the patient to: {0} month and {1} year") + public List cycleThroughPrenatalHistory() + { + + List loLabels = new ArrayList<>(); + + // It is difficult to specify more unique (readable) selectors for the latter two arguments as they will be searched + // for as children of the first argument's selector. We have similar selectors in cycleThroughAllPhenotypes() + // but they are a bit different due to how the tree structure is arranged. + // We add the strings together instead of ByChained becaues ByChained is itself a recursive tree traversal that + // will go deep and ignore the ">" to link the chain which means the "immediate child" (depth 1). + List loUncategorizedLabels = preOrderTraverseAndClick(By.cssSelector("div.prenatal-info"), + By.cssSelector("div.fieldset > " + yesBtnSelectorString), + By.cssSelector("div.fieldset > " + labelSelectorString)); + +// Expand the dropdowns for the yes/no options that are categorized into sections, lets them load first + preOrderTraverseAndClick( + By.cssSelector("div.prenatal-info > div > div > div"), expandToolSpan, expandToolSpan); + + List loCategorizedLabels = preOrderTraverseAndClick( + By.cssSelector( + "div.prenatal-info div[class=dropdown] div[class=entry-data], div.prenatal-info div[class*=term-entry]"), + By.cssSelector("span.yes-no-picker > label.yes"), + By.cssSelector("label.yes-no-picker-label, span.yes-no-picker-label > span.value")); + + loLabels.addAll(loUncategorizedLabels); + loLabels.addAll(loCategorizedLabels); + + return loLabels; + } + + /** + * Traverses through the rest of the boxes in prenatal options by entering in some text to each box. Traverses + * through the dropdowns too. Requires that the "Prenatal and perinatal history" section be expanded + * + * @return Stay on the same page so return the same object. + */ + @Step("Traverse through the prenatal options UI") + public CreatePatientPage cycleThroughPrenatalOptions() + { + waitForElementToBePresent(APGARScore1MinDrp); + + clickOnElement(termBirthCheckbox); + clickOnElement(termBirthCheckbox); + clickAndTypeOnElement(gestationWeeksBox, "12"); + clickAndTypeOnElement(maternalAgeBox, "26"); + clickAndTypeOnElement(paternalAgeBox, "30"); + + forceScrollToElement(APGARScore1MinDrp); + Select APGARScore1Min = new Select(superDriver.findElement(APGARScore1MinDrp)); + Select APGARScore5Min = new Select(superDriver.findElement(APGARScore5MinDrp)); + List loAPGARScores = new ArrayList<>(Arrays.asList( + "Unknown", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")); + + for (String score : loAPGARScores) { + APGARScore1Min.selectByVisibleText(score); + APGARScore5Min.selectByVisibleText(score); + } + + APGARScore1Min.selectByVisibleText("Unknown"); + APGARScore5Min.selectByVisibleText("Unknown"); + + clickAndTypeOnElement(otherPregnancyPhenotypeBox, "Tall chin"); + clickOnElement(firstGeneSuggestion); + + clickAndTypeOnElement(otherDevelopmentPhenotypeBox, "Tall chin"); + clickOnElement(firstGeneSuggestion); + + clickAndTypeOnElement(otherDeliveryPhenotypeBox, "Tall chin"); + clickOnElement(firstGeneSuggestion); + + clickAndTypeOnElement(otherGrowthPhenotypeBox, "Tall chin"); + clickOnElement(firstGeneSuggestion); + + clickAndTypeOnElement(otherComplicationsPhenotypeBox, "Tall chin"); + clickOnElement(firstGeneSuggestion); + + clickAndTypeOnElement(prenatalNotesBox, "Notes for prenatal. Moving on..."); + + return this; + } + + /***************************************** + * "Measurements" Section - Methods + *****************************************/ + + // TODO: Methods for this section only support one measurement entry right now. I don't have test cases for + // multiple measurement entries + + /** + * Adds a new entry of measurement data to the patient under the "Measurements" section. Requires: The + * "Measurements" section to already be expanded. + * + * @param aMeasurement is a measurement object (instantiated struct) containing all the measurement fields to + * enter. + * @return Stay on the same page so return the same object. + */ + @Step("Add a measurement to patient as: {0}") + public CreatePatientPage addMeasurement(CommonPatientMeasurement aMeasurement) + { + clickOnElement(addMeasurementBtn); + clickAndTypeOnElement(weightBox, String.valueOf(aMeasurement.weight)); + clickAndTypeOnElement(heightBox, String.valueOf(aMeasurement.height)); + clickAndTypeOnElement(armSpanBox, String.valueOf(aMeasurement.armSpan)); + clickAndTypeOnElement(sittingHeightBox, String.valueOf(aMeasurement.sittingHeight)); + clickAndTypeOnElement(headCircumferenceBox, String.valueOf(aMeasurement.headCircumference)); + clickAndTypeOnElement(philtrumLengthBox, String.valueOf(aMeasurement.philtrumLength)); + clickAndTypeOnElement(leftEarLengthBox, String.valueOf(aMeasurement.leftEarLength)); + clickAndTypeOnElement(rightEarLengthBox, String.valueOf(aMeasurement.rightEarLength)); + clickAndTypeOnElement(outherCanthalDistanceBox, String.valueOf(aMeasurement.outerCanthalDistance)); + clickAndTypeOnElement(innterCanthalDistanceBox, String.valueOf(aMeasurement.inntercanthalDistance)); + clickAndTypeOnElement(palpebralFissureLengthBox, String.valueOf(aMeasurement.palpebralFissureLength)); + clickAndTypeOnElement(interpupilaryDistanceBox, String.valueOf(aMeasurement.interpupilaryDistance)); + clickAndTypeOnElement(leftHandLengthBox, String.valueOf(aMeasurement.leftHandLength)); + clickAndTypeOnElement(leftPalmLengthBox, String.valueOf(aMeasurement.leftPalmLength)); + clickAndTypeOnElement(leftFootLengthBox, String.valueOf(aMeasurement.leftFootLength)); + clickAndTypeOnElement(rightHandLengthBox, String.valueOf(aMeasurement.rightHandLength)); + clickAndTypeOnElement(rightPalmLengthBox, String.valueOf(aMeasurement.rightPalmLength)); + clickAndTypeOnElement(rightFootLengthBox, String.valueOf(aMeasurement.rightFootLength)); + + return this; + } + + /** + * Changes the date of the first measurement to the specified month and year and date. Must be valid date otherwise + * Selenium will throw an ElementNotFound exception. Requires: The measurement section to be open and at least one + * measurement entry to be present. + * + * @param month is the month as a String "January" to "December". Must be exact. + * @param year is the year as a String "1920" to current year (ex. "2019"). Must be exact. + * @return Stay on the same page so return the same object. + */ + @Step("Change the measurement date to: {0} day {1} month and {2} year") + public CreatePatientPage changeMeasurementDate(String day, String month, String year) + { + By calendarDayBtn = By.xpath("//div[contains(text(), '" + day + "')]"); + + clickOnElement(measurementDateBoxes); + + waitForElementToBePresent(measurementMonthDrp); + Select monthDrp = new Select(superDriver.findElement(measurementMonthDrp)); + Select yearDrp = new Select(superDriver.findElement(measurementYearDrp)); + + monthDrp.selectByVisibleText(month); + yearDrp.selectByVisibleText(year); + + clickOnElement(calendarDayBtn); + waitForElementToBeGone(measurementMonthDrp); + + return this; + } + + /** + * Retrieves the first measurement entry for the patient that has measurement data entered. Requires: The + * "Measurement" section to be open and there be at least one measurmenet entry already there. + * + * @return A Measurement object constructed with the measurement data gathered from the patient. + */ + @Step("Retrieve the patients measurement") + public CommonPatientMeasurement getPatientMeasurement() + { + waitForElementToBePresent(weightBox); + + float weight = getSpecificMeasurement(weightBox); + float armSpan = getSpecificMeasurement(armSpanBox); + float headCircumference = getSpecificMeasurement(headCircumferenceBox); + float outerCanthalDistance = getSpecificMeasurement(outherCanthalDistanceBox); + float leftHandLength = getSpecificMeasurement(leftHandLengthBox); + float rightHandLength = getSpecificMeasurement(rightHandLengthBox); + + float height = getSpecificMeasurement(heightBox); + float sittingHeight = getSpecificMeasurement(sittingHeightBox); + float philtrumLength = getSpecificMeasurement(philtrumLengthBox); + float inntercanthalDistance = getSpecificMeasurement(innterCanthalDistanceBox); + float leftPalmLength = getSpecificMeasurement(leftPalmLengthBox); + float rightPalmLength = getSpecificMeasurement(rightPalmLengthBox); + + float leftEarLength = getSpecificMeasurement(leftEarLengthBox); + float palpebralFissureLength = getSpecificMeasurement(palpebralFissureLengthBox); + float leftFootLength = getSpecificMeasurement(leftFootLengthBox); + float rightFootLength = getSpecificMeasurement(rightFootLengthBox); + + float rightEarLength = getSpecificMeasurement(rightEarLengthBox); + float interpupilaryDistance = getSpecificMeasurement(interpupilaryDistanceBox); + + return new CommonPatientMeasurement( + weight, armSpan, headCircumference, outerCanthalDistance, leftHandLength, rightHandLength, + height, sittingHeight, philtrumLength, inntercanthalDistance, leftPalmLength, rightPalmLength, + leftEarLength, palpebralFissureLength, leftFootLength, rightFootLength, + rightEarLength, interpupilaryDistance); + } + + /** + * Retrieves the measurement value within the specified measurement box as a float. This is a helper function for + * getPatientMeasurement() + * + * @param measurementBoxSelector Selector of the specific box + * @return The float value of what was in the measurement box. If it were empty, returns 0. + */ + private float getSpecificMeasurement(By measurementBoxSelector) + { + return Float.parseFloat(superDriver.findElement(measurementBoxSelector).getAttribute("value")); + } + + /**************************************************************************** + * "Clinical symptoms and physical findings" (Phenotypes) Section - Methods + ****************************************************************************/ + /** + * Adds a phenotype by searching. Selects the first suggested phenotype search result. Assumes that "Clinical + * symptoms and physical findings" is expanded and there is at least one suggested search result. + * + * @param thePhenotype phenotype needed. Be exact and as specific as possible. + * @return Stay on the same page, so return same object. + */ + public CreatePatientPage addPhenotype(String thePhenotype) + { + clickAndTypeOnElement(phenotypeSearchBox, thePhenotype); + clickOnElement(firstPhenotypeSuggestion); + return this; + } + + /** + * Helper method to add a list of Phenotypes to the patient. Requires the "Clinical symptoms and physical findings" + * section is expanded and there is at least one suggested search result. + * + * @param phenotypes is a List of Strings specifying the phenotypes. You should be as exact as possible. + * @return Stay on the same page so return the same object. + */ + @Step("Add the following list of phenotypes: {0}") + public CreatePatientPage addPhenotypes(List phenotypes) + { + for (String aPhenotype : phenotypes) { + addPhenotype(aPhenotype); + } + return this; + } + + /** + * Gets all the labels for the labels within the Edit Phenotype Details box. This does not do a tree traversal due + * to the dropdowns having issues hiding/showing for now. Requires: A phenotype to already be present and "Add + * Details" to already be pressed so that the details box appears. + * + * @return A list of strings representing the labels found in the Edit Phenotype Details Box. This should not be + * empty. + */ + @Step("Traverse through the phenotype details UI section") + public List cycleThroughPhenotypeDetailsLabels() + { + waitForElementToBePresent(phenotypeDetailsLabels); + + List loExpandCarets = superDriver.findElements(expandCaretBtns); + List loLabels = new ArrayList<>(); + + //Expand all first + for (WebElement aCaret : loExpandCarets) { + // Should find this text, rather than rely on expansion selector due to PT-3095 + if (aCaret.getText().equals("â–º")) { + clickOnElement(aCaret); + } + } + + superDriver.findElements(phenotypeDetailsLabels).forEach(x -> loLabels.add(x.getText())); + + return loLabels; + } + + /** + * Adds phenotype details to the nth detail-less phenotype (done it this way due to the simplicity of + * implementation) in the list of phenotypes already present. Makes the grey phenotype details box appear. + * + * @param n is the nth phenotype WITHOUT any details added yet. + * @return Stay on the same page so return same object. + */ + @Step("Add details to the {0}th phenotype") + public CreatePatientPage addDetailsToNthPhenotype(int n) + { + waitForElementToBePresent(addPhenotypeDetailsBtns); + List loPhenotypeAddBtnsPresent = superDriver.findElements(addPhenotypeDetailsBtns); + + loPhenotypeAddBtnsPresent.get(n - 1).click(); + + waitForElementToBePresent(phenotypeDetailsLabels); + return this; + } + + /** + * Traverses through the options for phenotypes in the Clinical Symptoms and Physical Findings Section. This + * specific traversal only goes a depth level of 1 as the HPO tree is rather large. Maybe split to several smaller + * functions for each HPO section and recurse full tree if we want a more detailed sanity test. Requires: The + * "Clinical Symptoms and Physical Findings" section to be expanded and that none of the yes/no options are already + * selected/expanded (i.e. should be at the state of a new patient) Otherwise, traversal result might be off due to + * presence of additional (appearing) selectors. + * + * @return a List of Strings which represent the health conditions found under the yes/no boxes of "Clinical + * Symptoms and Physical Findings" + */ + @Step("Traverse (pre-order) through all phenotypes. Depth level limited to 1.") + public List cycleThroughAllPhenotypes() + { + + clickOnElement(By.cssSelector("span.expand-all")); + + List loLabels = new ArrayList<>(); + + forceScrollToElement(By.cssSelector("div.phenotype > div > div > div")); + +// Expand all dropdowns, lets them load first + preOrderTraverseAndClick( + By.cssSelector("div.phenotype > div > div > div"), expandToolSpan, expandToolSpan); + + List loCategorizedLabels = preOrderTraverseAndClick( + By.cssSelector( + "div.phenotype div[class=dropdown] div[class=entry-data], div.prenatal-info div[class*=term-entry]"), + By.cssSelector("span.yes-no-picker > label.na"), + By.cssSelector("label.yes-no-picker-label, span.yes-no-picker-label > span.value")); + + loLabels.addAll(loCategorizedLabels); + + return loLabels; + } + + /** + * Retrieves the phenotypes specified by the passed selector. This is a helper method that is used to differentiate + * between different phenotypes such as ones automatically added via measurements data vs. manual input Requires: + * The "Clinical symptoms and physical findings" section to be expanded + * + * @param phenotypeLabelsSelector The By selector of the phenotype label. There are three so far: - + * phenotypesSelectedLabels (all), - phenotypesAutoSelectedByMeasurementLabels (lightning bolt/auto ones), - + * phenotypesManuallySelectedLabels (non-lightning bolt/manual ones) + * @return A List of Strings representing the names of the phenotypes found. Can potentially be empty. + */ + private List getPresentPhenotypes(By phenotypeLabelsSelector) + { + List loPhenotypesFound = new ArrayList<>(); + waitForElementToBePresent(phenotypeSearchBox); + superDriver.findElements(phenotypeLabelsSelector).forEach(x -> loPhenotypesFound.add(x.getText())); + + return loPhenotypesFound; + } + + /** + * Retrieves all of the phenotypes already present (entered) in the Patient Information Form Requires: The "Clinical + * symptoms and physical findings" section to be expanded + * + * @return A (potentially empty) list of Strings representing the names of the phenotypes found. + */ + @Step("Retrieve all phenotypes") + public List getAllPhenotypes() + { + return getPresentPhenotypes(phenotypesSelectedLabels); + } + + /** + * Retrieves a list of phenotypes entered automatically due to measurement data. These are the phenotypes with the + * lightning symbol beside them. Requires: The "Clinical symptoms and physical findings" section to be expanded + * + * @return A, possibly empty, list of Strings representing phenotypes that have a lightning symbol beside them due + * to them being automatically added from data contained on a measurements entry. + */ + @Step("Retrieve phenotypes added automatically due to measurements (lightning icon beside them)") + public List getPhenotypesLightning() + { + return getPresentPhenotypes(phenotypesAutoSelectedByMeasurementLabels); + } + + /** + * Retrieves a list of phenotypes that were manually entered. These are the ones that are not prefixed with a + * lightning symbol. Requires: The "Clinical symptoms and physical findings" section to be expanded + * + * @return A List of Strings, possibly empty, of the phenotypes that were manually entered (do not have a lightning + * symbol in front of them). + */ + @Step("Retrieve phenotypes that were added manually (no lightning icon beside them)") + public List getPhenotypesNonLightning() + { + return getPresentPhenotypes(phenotypesManuallySelectedLabels); + } + + /******************************************** + * "Genotype Information" Section - Methods + ********************************************/ + /** + * Adds a gene to the "Genotype information" section, using the gene name, status, and strategy. Clicks the "add + * gene" button before adding to the bottom-most row. Searches for gene and selects first suggestion. Assumes that + * "Genotype information" is already expanded (selectors visible) and at least one gene is returned in search + * results. + * + * @param theGene name of the gene, be as exact and specific as possible. + * @param geneStatus status of the gene. One of: "Candidate", "Rejected candidate", "Confirmed causal", "Carrier", + * and "Tested negative". Must be exact, will throw exception otherwise. + * @param strategy specifies which Gene strategy checkbox to toggle. One of: "Sequencing", "Deletion/duplication", + * "Familial mutation", and "Common mutations". Must be exact. Defaults to no strategy to specify. + * @return Stay on the same page so we return the same object. + */ + @Step("Add gene: {0} with status: {1} and strategy: {2}") + public CreatePatientPage addGene(String theGene, String geneStatus, String strategy) + { + forceScrollToElement(addGeneBtn); + clickOnElement(addGeneBtn); + + waitForElementToBePresent(geneNameBoxes); // Must wait before search for elements + unconditionalWaitNs(1); + + List foundGeneBoxes = superDriver.findElements(geneNameBoxes); + List foundStatusDrps = superDriver.findElements(geneStatusDrps); + + // Get the last element of each list for the most bottom one + WebElement bottommostGeneNameBox = foundGeneBoxes.get(foundGeneBoxes.size() - 1); + WebElement bottommostStatusDrp = foundStatusDrps.get(foundStatusDrps.size() - 1); + + Select statusDrp = new Select(bottommostStatusDrp); + + bottommostGeneNameBox.click(); + bottommostGeneNameBox.sendKeys(theGene); + clickOnElement(firstGeneSuggestion); + + bottommostStatusDrp.click(); + statusDrp.selectByVisibleText(geneStatus); + + List foundDesiredStrategyCheckboxes; + + switch (strategy) { + case "Sequencing": + foundDesiredStrategyCheckboxes = superDriver.findElements(geneStrategySequencingCheckboxes); + foundDesiredStrategyCheckboxes.get(foundDesiredStrategyCheckboxes.size() - 1).click(); + break; + + case "Deletion/duplication": + foundDesiredStrategyCheckboxes = superDriver.findElements(geneStrategyDeletionCheckboxes); + foundDesiredStrategyCheckboxes.get(foundDesiredStrategyCheckboxes.size() - 1).click(); + break; + + case "Familial mutation": + foundDesiredStrategyCheckboxes = superDriver.findElements(geneStrategyFamilialMutationCheckboxes); + foundDesiredStrategyCheckboxes.get(foundDesiredStrategyCheckboxes.size() - 1).click(); + break; + + case "Common mutations": + foundDesiredStrategyCheckboxes = superDriver.findElements(geneStrategyCommonMutationCheckboxes); + foundDesiredStrategyCheckboxes.get(foundDesiredStrategyCheckboxes.size() - 1).click(); + break; + + default: + System.out.println("Invalid gene strategy passed to addGene(). No strategy checked."); + break; + } + + return this; + } + + /********************************** + * "Diagnosis" Section - Methods + **********************************/ + /** + * Adds a clinical diagnosis in the "Diagnosis" section to the patient. Selects the first result from the suggestion + * dropdown. Requires: Diagnosis section to be expanded and the ORDO name to be as exact as possible to the + * suggestions list. + * + * @param ORDO the name of the diagnosis, either the ID number or the name of the diagnosis. + * @return Stay on the same page so return the same object. + */ + @Step("Add a clinical diagnosis with ORDO: {0}") + public CreatePatientPage addClinicalDiagnosis(String ORDO) + { + clickAndTypeOnElement(clinicalDiagnosisBox, ORDO); + clickOnElement(firstGeneSuggestion); + return this; + } + + /** + * Adds a final diagnosis in the "Diagnosis" section to the patient. Selects the first result in the suggestions + * dropdown. Requires the "Diagnosis" section to be expanded. + * + * @param OMIM is the name of the diagnosis or the OMIM number, as a String. Should be as exact as possible. + * @return Stay on the same page so return the same object. + */ + @Step("Add final diagnosis with OMIM: {0}") + public CreatePatientPage addFinalDiagnosis(String OMIM) + { + clickAndTypeOnElement(finalDiagnosisBox, OMIM); + clickOnElement(firstGeneSuggestion); + return this; + } + + /** + * Toggles the "Case Solved" checkbox in the "Diagnosis" section. Requires the "Diagnosis" section to be expanded. + * + * @return Stay on the same page so return the same object. + */ + @Step("Toggle the 'Case Solved' checkbox") + public CreatePatientPage toggleCaseSolved() + { + clickOnElement(caseSolvedCheckbox); + return this; + } + + /** + * Determines if the "Case Solved" checkbox under "Diagnosis" section is enabled or not. Requires the "Diagnosis" + * section to be expanded. + * + * @return A boolean, true for checked and false for unchecked. + */ + @Step("Determine if the case is solved") + public boolean isCaseSolved() + { + waitForElementToBePresent(caseSolvedCheckbox); + return superDriver.findElement(caseSolvedCheckbox).isSelected(); + } + + /** + * Adds a PubMed ID to the bottom-most PubMed ID box there is. If there is a pubmed article on the page, will add a + * new row by clicking on the add pubMed ID link. Requires the "Diagnosis" section to be expanded and the "Case + * Solved" checkbox to be enabled. + * + * @param ID is the pubMed ID (8 digit number) to add to the patient + * @return Stay on the same page so return the same object. + */ + @Step("Add a PubMed ID of {0}") + public CreatePatientPage addPubMedID(String ID) + { + clickOnElement(additionalCommentsBox); // Needed to defocus PubMed ID boxes. + + if (isElementPresent(pubMDArticle)) { + clickOnElement(addPubMDLink); + } + + List loPubMDIDBoxes = superDriver.findElements(pubMDIDBoxes); + clickOnElement(loPubMDIDBoxes.get(loPubMDIDBoxes.size() - 1)); + loPubMDIDBoxes.get(loPubMDIDBoxes.size() - 1).sendKeys(ID); + + clickOnElement(additionalCommentsBox); + + return this; + } + + /** + * Removes the Nth PubMed entry from the Diagnosis section. Requires that there be at least n PubMed entries and the + * "Diagnosis" section to be expanded. This should not be called if there are no existing PubMed entries. + * + * @param n is int >= 1. + * @return Stay on the same page so return the same object. + */ + @Step("Remove the {0}th PubMed ID") + public CreatePatientPage removeNthPubMedID(int n) + { + waitForElementToBePresent(deletePubMDBtns); + List loDeletePubMDBtns = superDriver.findElements(deletePubMDBtns); + + clickOnElement(loDeletePubMDBtns.get(n - 1)); + + return this; + } + + /** + * Adds a note to the Resolution Notes box under "Diagnosis" section. Concatenates to what is already present in + * that box. Requires: Diagnosis section to already be expanded. + * + * @param note is a String representing the note you need to enter. + * @return Stay on the same page so return the same object. + */ + @Step("Add resolution notes of {0}") + public CreatePatientPage addResolutionNotes(String note) + { + clickAndTypeOnElement(resolutionNotesBox, note); + return this; + } + + /** + * Adds a note to the Additional Comments box under "Diagnosis" section. Concatenates to what is already present in + * that box. + * + * @param comment is the comment to add, as an arbitrary String + * @return Stay on the same page so return the same object. + */ + @Step("Add additional comments in diagnosis section called {0}") + public CreatePatientPage addAdditionalComments(String comment) + { + clickAndTypeOnElement(additionalCommentsBox, comment); + return this; + } + + /** + * Determines if the nth PubMed ID box has valid input. Locates the red error message that is given in the case of + * invalid input. Requires the "Diagnosis" section to be expanded and that there be at least n PubMed IDs already + * entered. + * + * @param n is the Nth PubMed ID box. + * @return A boolean indicating validity, true for valid (summary of article shown) and false for invalid (the error + * message is displayed). + */ + @Step("Determine if {0}th PubMedID box has valid input") + public boolean isNthPubMDBoxValid(int n) + { + final String invalidPubMedIDError = "Invalid Pubmed ID"; + + clickOnElement(additionalCommentsBox); // Needed so that pubMed boxes goes out of focus and does validation + waitForElementToBePresent(pubMDIDCheckStatus); + + String statusText = superDriver.findElements(pubMDIDCheckStatus).get(n - 1).getText(); + + return !statusText.equals(invalidPubMedIDError); + } + + /** + * Tries to modify each input box within the "Diagnosis" section. Adds something to each box. + * + * @return Stay on the same page so return the same object. + */ + @Step("Traverse through the diagnosis section UI") + public CreatePatientPage cycleThroughDiagnosisBoxes() + { + addClinicalDiagnosis("Allergic bronchopulmonary aspergillosis"); + addClinicalDiagnosis("Essential iris atrophy"); + toggleNthClinicalDiagnosisCheckbox(1); + toggleNthClinicalDiagnosisCheckbox(1); + toggleNthClinicalDiagnosisCheckbox(2); + toggleNthClinicalDiagnosisCheckbox(2); + addFinalDiagnosis("ALLERGIC RHINITIS"); + addFinalDiagnosis("KOOLEN-DE VRIES SYNDROME"); + toggleNthFinalDiagnosisCheckbox(1); + toggleNthFinalDiagnosisCheckbox(1); + toggleNthFinalDiagnosisCheckbox(2); + toggleNthFinalDiagnosisCheckbox(2); + addAdditionalComments("Comment in Additional Comments"); + toggleCaseSolved(); + addPubMedID("30699054"); + addPubMedID("30699052"); + addResolutionNotes("Resolved"); + + return this; + } + + /** + * Toggles the Nth Clinical Diagnosis checkbox within the list of added clinical diagnosis. Requires the "Diagnosis" + * section to be expanded and there at least be n ORDOs already listed. + * + * @param n is int >= 1, indicating the Nth clinical diagnosis checkbox to toggle. + * @return Stay on the same page so return the same object. + */ + @Step("Toggle the {0} clinical diagnosis checkbox.") + public CreatePatientPage toggleNthClinicalDiagnosisCheckbox(int n) + { + waitForElementToBePresent(clinicalDiagnosisCheckboxes); + clickOnElement(superDriver.findElements(clinicalDiagnosisCheckboxes).get(n - 1)); + + return this; + } + + /** + * Toggles the Nth Final Diagnosis checkbox within the list of Final Diagnosis already added. Requires the + * "Diagnosis" section to be expanded and there at least be n OMIMs listed already. + * + * @param n is integer >= 1, indicating the Nth Final Diagnosis checkbox to toggle. + * @return Stay on the same page so return the same object. + */ + @Step("Toggle the {0}th final diagnosis checkbox") + public CreatePatientPage toggleNthFinalDiagnosisCheckbox(int n) + { + waitForElementToBePresent(finalDiagnosisCheckboxes); + clickOnElement(superDriver.findElements(finalDiagnosisCheckboxes).get(n - 1)); + + return this; + } + + /** + * Checks for the visibility (i.e. clickability) of the pubMed ID boxes and Resolution Notes box. Visibility is not + * enough as the elements are always loaded but with odd hidden property that is still considered visible. Requires + * the "Diagnosis" section to be expanded. + * + * @return bool indicating the presence of those two boxes. True for present, and false if not present. + */ + @Step("Determine if the PubMed and Resolution boxes are clickable") + public boolean isPubMedAndResolutionBoxesClickable() + { + return (isElementClickable(pubMDIDBoxes) && (isElementClickable(resolutionNotesBox))); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/EmailUIPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/EmailUIPage.java new file mode 100644 index 00000000..aaf3cd29 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/EmailUIPage.java @@ -0,0 +1,96 @@ +package org.phenotips.endtoendtests.pageobjects; + +import java.util.List; +import java.util.Vector; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import io.qameta.allure.Step; + +/** + * This is the webpage of the fake SMTP service's UI. In this case, we are using MockMock. As it provides a web + * interface for the email inbox. Assumes that the fake SMTP service is running as per the Readme. + */ +public class EmailUIPage extends BasePage +{ + private final By deleteAllEmailsLink = By.cssSelector(".delete"); + + private final By emailStatus = By.cssSelector("div.container:nth-of-type(2) > *:first-child"); + + private final By emailRows = By.cssSelector("table > tbody > tr"); + + private final By emailTitles = By.cssSelector("table > tbody > tr > td:nth-child(3) > a"); + + public EmailUIPage(WebDriver aDriver) + { + super(aDriver); + } + + /** + * Deletes all emails, if any, in the inbox. + * + * @return stay on the same page so return the same object. + */ + @Step("Delete all emails from inbox") + public EmailUIPage deleteAllEmails() + { + if (getNumberOfEmails() > 0) { + unconditionalWaitNs(1); // Wait is only used so that a human can see the emails. + clickOnElement(deleteAllEmailsLink); + } else { + System.out.println("There are no emails to delete!"); + } + return this; + } + + /** + * Get a (possibly empty) list of titles of emails from the inbox. + * + * @return a List of Strings which are titles of all the emails. + */ + @Step("Retrieve a list of email titles") + public List getEmailTitles() + { + List loTitles = new Vector(); + + unconditionalWaitNs(1); // Again, this wait was just for human readability + if (getNumberOfEmails() > 0) { + waitForElementToBePresent(emailTitles); + superDriver.findElements(emailTitles).forEach(x -> loTitles.add(x.getText())); + } + return loTitles; + } + + /** + * Indicates the number of emails in the inbox. Checks the main text for the overall status first. + * + * @return integer >= 0. + */ + @Step("Retrieve the number of emails in the inbox") + public int getNumberOfEmails() + { + waitForElementToBePresent(emailStatus); + String emailText = superDriver.findElement(emailStatus).getText(); + if (emailText.contains("You have ")) { + return superDriver.findElements(emailRows).size(); + } else { + return 0; + } + } + + /** + * Navigates to the homepage of PC, regardless of whether or not user is logged in. This is needed as we need a way + * to go from MockMock UI back to a PC page. Navigates directly and explicitly to the homepage url. We are + * overriding the BasePage method since this does not try to click the PC logo that appears in the top left. + * + * @return a new HomePage object as we navigate there. + */ + @Override + @Step("Navigate to the PC instance's homepage") + public HomePage navigateToHomePage() + { + superDriver.navigate().to(HOMEPAGE_URL); + return new HomePage(superDriver); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/HomePage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/HomePage.java new file mode 100644 index 00000000..6b24623b --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/HomePage.java @@ -0,0 +1,107 @@ +package org.phenotips.endtoendtests.pageobjects; + +import java.util.ArrayList; +import java.util.List; + +import org.openqa.selenium.By; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.WebDriver; + +import io.qameta.allure.Step; + +/** + * Represents the page on http://localhost:8083/ (HOMEPAGE_URL) + */ +public class HomePage extends BasePage +{ + final By loginLink = By.id("launch-login"); + + final By signUpButton = By.id("launch-register"); + + final By sectionTitles = By.cssSelector("div.gadget-title"); // Titles of the sections visible on the splash page + + final By unauthorizedActionErrorMsg = By.cssSelector("p.xwikimessage"); + // Error message that users get when trying to view patients that aren't theirs. + + public HomePage(WebDriver aDriver) + { + super(aDriver); + } // Give the webdriver to the superclass + + /** + * Go to the login page where a user enters their credentials. It tries to logout when it cannot find the login link + * on the homepage. This should result in the login page being displayed. Ideally, this gets called from the splash + * page that the public sees when on the home page. Ex. "Enter cases, find matches, and connect with other rare + * disease specialists. Find out more..." + * + * @return a new login page object as we navigate there + */ + @Step("Navigate to login page") + public LoginPage navigateToLoginPage() + { + superDriver.navigate().to(HOMEPAGE_URL); + // Try to click on the link immediately, rather than checking for a logOut link + // being present. That causes five seconds of whenever trying to navigate to login page. + try { + clickOnElement(loginLink); + } catch (TimeoutException e) { + logOut(); + } + + return new LoginPage(superDriver); + } + + /** + * Navigate to the User Sign up page by clicking on the "Sign Up" button from the homepage. This is the public sign + * up page form where people can request access to the PC instance. Ideally, the no user should be signed in when + * calling this method. + * + * @return A new instance of the UserSignUp page as we navigate there. + */ + @Step("Navigate to sign up page") + public UserSignUpPage navigateToSignUpPage() + { + superDriver.navigate().to(HOMEPAGE_URL); + if (isElementPresent(logOutLink)) { + logOut(); + } + clickOnElement(signUpButton); + + return new UserSignUpPage(superDriver); + } + + /** + * Retrieves a list of section titles that appear when a user logs in. These are the individual widgets that are + * present right after logging in (on the home page). As of now, they are: "My Matches", "My Patients", "Patients + * Shared With Me", "My Groups" and "Public Data" This is useful for determining if the patient has privilages that + * are granted upon user approval. Without approval, they should see none of those headings. + * + * @return A list of Strings representing the titles of each section. + */ + @Step("Retrieve titles of each section on the splash page") + public List getSectionTitles() + { + waitForElementToBePresent(logOutLink); + List loTitles = new ArrayList<>(); + + superDriver.findElements(sectionTitles).forEach(x -> loTitles.add(x.getText())); + + return loTitles; + } + + /** + * Retrieve the unauthorized action error message that a user gets. For instance, when trying to view a patient that + * is not theirs and is not public. "You are not allowed to view this page or perform this action." Requires: That + * the error message page be present, otherwise will throw exception that error is not found. Exception when there + * is no error, hence test failure. + * + * @return A String representing the message. + */ + @Step("Retrieve the unauthorized access error message 'You are not allowed to view this page or perform this action.'") + public String getUnauthorizedErrorMessage() + { + waitForElementToBePresent(unauthorizedActionErrorMsg); + + return superDriver.findElement(unauthorizedActionErrorMsg).getText(); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/LoginPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/LoginPage.java new file mode 100644 index 00000000..87a12e14 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/LoginPage.java @@ -0,0 +1,74 @@ +package org.phenotips.endtoendtests.pageobjects; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import io.qameta.allure.Step; + +/** + * Represents the page http://localhost:8083/PhenomeCentral/login This is the user login page + */ +public class LoginPage extends BasePage +{ + private final By userNameField = By.id("j_username"); + + private final By passField = By.id("j_pasword"); // Note: There might be a typo there + + private final By loginButton = By.cssSelector("input.button[value='Sign in']"); + + public LoginPage(WebDriver aDriver) + { + super(aDriver); + } + + /** + * Logs in using username and password. Assumes that login will be sucessful. + * + * @param username non-empty case sensitive user ID + * @param password non-empty and case sensitive password + * @return a homepage object as we navigate there on successful login. + */ + @Step("Login with credentials username {0} and password {1}") + public HomePage loginAs(String username, String password) + { + clickAndTypeOnElement(userNameField, username); + clickAndTypeOnElement(passField, password); + + clickOnElement(loginButton); + + return new HomePage(superDriver); + } + + /** + * Logs in with the default admin credentials + * + * @return a homepage object as we navigate there upon sucessful login + */ + @Step("Login as an admin") + public HomePage loginAsAdmin() + { + return loginAs(ADMIN_USERNAME, ADMIN_PASS); + } + + /** + * Logs in with a regular user's credentials. + * + * @return a homepage object as we navigate there upon sucessful login + */ + @Step("Login as User 1") + public HomePage loginAsUser() + { + return loginAs(USER_USERNAME, USER_PASS); + } + + /** + * Logs in with the second user's credentials. + * + * @return a homepage object as we navigate there upon sucessful login + */ + @Step("Login as User 2") + public HomePage loginAsUserTwo() + { + return loginAs(USER_USERNAME2, USER_PASS2); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/PedigreeEditorPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/PedigreeEditorPage.java new file mode 100644 index 00000000..3bd45351 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/PedigreeEditorPage.java @@ -0,0 +1,640 @@ +package org.phenotips.endtoendtests.pageobjects; + +import java.util.ArrayList; +import java.util.List; + +import org.openqa.selenium.By; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; + +import io.qameta.allure.Step; + +/** + * This represents the pedigree editor page, i.e. http://localhost:8083/edit/Families/FAMxxxxxxx + */ +public class PedigreeEditorPage extends BasePage +{ + private final By probandTemplate = By.cssSelector("div[title=Proband]"); + + /************************************** + * Toolbar and save modal - Selectors + **************************************/ + private final By closeEditor = By.id("action-close"); + + private final By saveAndQuitBtn = By.id("OK_button"); + + private final By dontSaveAndQuitBtn = By.cssSelector("input.button[value=\" Don't save and quit \"]"); + + private final By keepEditingPedigreeBtn = By.cssSelector("input.button[value=\" Keep editing pedigree \"]"); + + /********************************************************************** + * Patient Nodes (Hoverboxes) and Add Family Member Links - Selectors + **********************************************************************/ + private final By hoverBox = By.cssSelector("#work-area > #canvas > svg > rect.pedigree-hoverbox"); + + private final By hoverBoxPartnerLink = By.cssSelector( + "#work-area > #canvas > svg > rect.pedigree-hoverbox[width=\"52\"]"); + // Actually, no, maybe we shouldn't have all rectangle classes. I think circles would cause it to break. + // We should figure out how to traverse up the structure of nodes. Maybe add IDs to each node. + // It looks like its just a linear list of nodes for now... so we have to get selectors by grepping weird values + + private final By createSiblingNode = By.cssSelector("rect[transform^=\"matrix(0.7071,0.7071,-0.7071,0.7071\"]"); + + private final By createChildNode = By.cssSelector( + "rect[transform=\"matrix(0.7071,0.7071,-0.7071,0.7071,70.6066,-20.4594)\"]"); + + private final By createPartnerNode = By.xpath( + "//*[@*=\"Click to draw a partner or drag to an existing person to create a partnership (valid choices will be highlighted in green)\"]"); + + private final By createParentNode = By.xpath( + "//a[contains(@title, 'Click to draw parents or drag to an existing person or partnership (valid choices will be highlighted in green). Dragging to a person will create a new relationship.'"); + + private final By linkedPatientIDLink = By.cssSelector( + "text.pedigree-nodePatientTextLink > tspan"); + + private final By createMaleNode = By.cssSelector("a.node-type-M"); + + /********************************************************************************** + * Patient Information Form Modal (Opens when clicking on a patient) - Selectors + **********************************************************************************/ + + // Tabs + private final By personalTab = By.cssSelector("div.person-node-menu > div.tabholder > dl.tabs > dd:nth-child(1)"); + + private final By clinicalTab = By.cssSelector("div.person-node-menu > div.tabholder > dl.tabs > dd:nth-child(2)"); + + private final By cancersTab = By.cssSelector("div.person-node-menu > div.tabholder > dl.tabs > dd:nth-child(3)"); + + // Personal Tab + private final By linkPatientBox = By.cssSelector("input.suggest-patients"); + + private final By linkPatientFirstSuggestion = By.cssSelector("span.suggestValue"); // First suggestion + + private final By createNewPatientBtn = By.cssSelector("span.patient-create-button"); + + private final By confirmNewPatientBtn = By.cssSelector("input[value=Confirm]"); + + private final By patientIDInModal = By.cssSelector("div.patient-link-container > a.patient-link-url"); + + private final By maleGenderBtn = By.cssSelector("input[value=M]"); + + private final By femaleGenderBtn = By.cssSelector("input[value=F]"); + + private final By otherGenderBtn = By.cssSelector("input[value=O]"); + + private final By unknownGenderBtn = By.cssSelector("input[value=U]"); + + private final By identifierBox = By.cssSelector("input[name=external_id]"); + + private final By ethnicitiesList = By.cssSelector("div.field-ethnicity > ul.accepted-suggestions > li"); + + private final By ethnicitiesBox = By.cssSelector("#tab_Personal > div.field-ethnicity > input[name=ethnicity]"); + + private final By aliveRadioBtn = By.cssSelector("label.state_alive > input[value=alive]"); + + private final By deceasedRadioBtn = By.cssSelector("label.state_deceased > input[value=deceased]"); + + private final By unbornRadioBtn = By.cssSelector("label.state_unborn > input[value=unborn]"); + + private final By stillbornRadioBtn = By.cssSelector("label.state_stillborn > input[value=stillborn]"); + + private final By miscarriedRadioBtn = By.cssSelector("label.state_miscarriage > input[value=miscarriage]"); + + private final By abortedRadioBtn = By.cssSelector("label.state_aborted > input[value=aborted]"); + + private final By aliveAndWellCheckbox = By.cssSelector("div.field-aliveandwell input[name=aliveandwell]"); + + private final By dobYearDrp = By.cssSelector("div.field-date_of_birth > div > div > span > select[title=year]"); + + private final By dobMonthDrp = By.cssSelector("div.field-date_of_birth > div > div > span > select[title=month]"); + + // Clinical Tab + private final By phenotypesList = By.cssSelector( + "div.field-hpo_positive > ul.accepted-suggestions > li > label.accepted-suggestion > span.value"); + + private final By phenotypeBox = By.cssSelector("input.suggest-hpo"); + + private final By candidateGeneBox = By.cssSelector("div.field-candidate_genes > input.suggest-genes"); + + private final By candidateGeneList = By.cssSelector( + "div.field-candidate_genes > ul.accepted-suggestions > li > label.accepted-suggestion > span.value"); + + private final By causalGeneBox = By.cssSelector("div.field-causal_genes > input.suggest-genes"); + + private final By causalGeneList = By.cssSelector( + "div.field-causal_genes > ul.accepted-suggestions > li > label.accepted-suggestion > span.value"); + + private final By carrierGeneBox = By.cssSelector("div.field-carrier_genes > input.suggest-genes"); + + private final By carrierGeneList = By.cssSelector( + "div.field-carrier_genes > ul.accepted-suggestions > li > label.accepted-suggestion > span.value"); + + public PedigreeEditorPage(WebDriver aDriver) + { + super(aDriver); + + // Try to click on the default Proband template. If there is no template modal present, catch the error + // and just assume that there was no modal in the first place. + try { + unconditionalWaitNs(5); + clickOnElement(probandTemplate); + waitForElementToBeClickable(hoverBox); + } catch (TimeoutException e) { + System.out.println("Seems like we are editing an existing pedigree, no template dialogue found."); + } + } + + /******************** + * Toolbar methods + ********************/ + + /** + * Closes the editor and handles the warning dialogue if it appears. Requires that no modals are blocking the + * pedigree toolbar beforehand (ex. the template selection modal). + * + * @param saveChoice is String representing the choice of save. It must be exactly one of "Save", "Don't Save", + * "Keep Editing". Defaults to "Save" on invalid string. + * @return Navigates back to the patient creation page so a return new instance of that. + */ + @Step("Close the editor with save choice of {0}") + public CreatePatientPage closeEditor(String saveChoice) + { + clickOnElement(closeEditor); + + if (isElementPresent(saveAndQuitBtn)) { + switch (saveChoice) { + case "Save": + clickOnElement(saveAndQuitBtn); + break; + case "Don't Save": + clickOnElement(dontSaveAndQuitBtn); + break; + case "Keep Editing": + clickOnElement(keepEditingPedigreeBtn); + break; + default: + System.out.println("Invalid saveChoice in closeEditor, default to Save"); + clickOnElement(saveAndQuitBtn); + break; + } + } + + waitForElementToBePresent(logOutLink); // We should wait for this to appear. + + return new CreatePatientPage(superDriver); + } + + /** + * Checks if a warning dialogue appears when trying to close the editor. Clicks on close and then tries to click on + * "Save and Quit". Sometimes, the dialogue isn't supposed to be there so it would have navigated away with no + * warning. Waits for the three buttons to appear. Requires that the pedigree toolbar be interactable, not blocked + * by some other modal. + * + * @return A Boolean to indicate whether the dialogue, or more specifically, "keep editing pedigree" button appears + * or not. State afterwards would be navigation away from the Pedigree Editor to Patient Creation page + */ + @Step("Determine if the warning dialogue appeared") + public Boolean doesWarningDialogueAppear() + { + clickOnElement(closeEditor); + + waitForElementToBePresent(saveAndQuitBtn); + waitForElementToBePresent(dontSaveAndQuitBtn); + waitForElementToBePresent(keepEditingPedigreeBtn); + + Boolean appearance = isElementPresent(saveAndQuitBtn); + if (appearance) { + clickOnElement(saveAndQuitBtn); + } + // else, would have navigated back to create patient page. + return appearance; + } + + /*********************************************** + * Patient Information Form (Modal) - Methods + ***********************************************/ + /** + * Switches the tab on the current patient info modal. + * + * @param infoTab is one of three Strings: "Personal", "Clinical", "Cancers", each corresponding to a tab on the + * modal. Upon invalid string entry, goes to the Personal tab. + * @return stay on the same page so return same object + */ + @Step("Switch to tab: {0}") + public PedigreeEditorPage switchToTab(String infoTab) + { + switch (infoTab) { + case "Personal": + clickOnElement(personalTab); + waitForElementToBePresent(ethnicitiesBox); + break; + case "Clinical": + clickOnElement(clinicalTab); + waitForElementToBePresent(phenotypeBox); + break; + case "Cancers": + clickOnElement(cancersTab); + break; // Should add wait here + default: + clickOnElement(personalTab); + waitForElementToBePresent(ethnicitiesBox); + break; + } + return this; + } + + /**************************************************** + * Personal Tab (Patient Information Modal) Methods + ****************************************************/ + /** + * Retrieves the patient ID of the currently open patient modal. Requires that the patient to already be linked, + * otherwise will cause an exception where element is not found. TODO: Improve this so that it returns some other + * string when no patient is linked. + * + * @return a String in the form of Pxxxxxxxx + */ + @Step("Retrieve the patient ID from the modal") + public String getPatientIDFromModal() + { + waitForElementToBePresent(patientIDInModal); + return superDriver.findElement(patientIDInModal).getText(); + } + + /** + * Links the currently focused node to an existing patient via the "Link to an existing patient record" box in the + * "Personal" tab. Requires a patient's information modal to be open. + * + * @param patientID is either "New" to indicate "Create New" should be clicked or a patient ID in form of Pxxxxxxx + * @return Stay on the same page so return same object. + */ + @Step("Link this node to patient ID: {0}") + public PedigreeEditorPage linkPatient(String patientID) + { + switchToTab("Personal"); + + if (patientID.equals("New")) { + clickOnElement(createNewPatientBtn); + clickOnElement(confirmNewPatientBtn); + waitForElementToBeClickable(personalTab); + unconditionalWaitNs( + 5); // PersonalTab is deemed clickable too early? Loading modal has not fully disappeared. + } else { + clickAndTypeOnElement(linkPatientBox, patientID); + clickOnElement(linkPatientFirstSuggestion); + } + + return this; + } + + /** + * Returns the current gender that is selected in the radio button options. + * + * @return a String representing the gender, one of: "Male", "Female", "Other", "Unknown" + */ + @Step("Retrieve the gender of the patient") + public String getGender() + { + if (superDriver.findElement(maleGenderBtn).isSelected()) { + return "Male"; + } else if (superDriver.findElement(femaleGenderBtn).isSelected()) { + return "Female"; + } else if (superDriver.findElement(otherGenderBtn).isSelected()) { + return "Other"; + } else { + return "Unknown"; + } + } + + /** + * Sets the gender of the patient that has its information form modal open. Requires: The Patient Information Form + * modal to be open for a patient. + * + * @param gender is the desired gender to set. Must be exact or defaults to Female. One of: "Male", "Female", + * "Other", or "Unknown". + * @return Stay on the same page so return the same object. + */ + @Step("Set the gender of the patient to {0}") + public PedigreeEditorPage setGender(String gender) + { + clickOnElement(clinicalTab); + + switch (gender) { + case "Male": + clickOnElement(maleGenderBtn); + break; + case "Female": + clickOnElement(femaleGenderBtn); + break; + case "Other": + clickOnElement(otherGenderBtn); + break; + case "Unknown": + clickOnElement(unknownGenderBtn); + break; + default: + clickOnElement(femaleGenderBtn); + break; + } + return this; + } + + /** + * Retrieves the ethnicities of the patient listed in the pedigree editor in the "Personal" tab. Requires a + * patient's information modal to be present. + * + * @return A, possibly empty, list of Strings representing the ethnicities that were found. + */ + @Step("Retrieve the ethnicities of the patient") + public List getEthnicities() + { + switchToTab("Personal"); + return getLabelsFromList(ethnicitiesList); + } + + /***************************************************** + * Clinical Tab (Patient Information Modal) Methods + *****************************************************/ + /** + * Retrieves a list of entered phenotypes within the "Clinical" tab. Requires the patient information modal to be + * open. + * + * @return A, possibly empty, list of Strings containing the names of the phenotypes. + */ + @Step("Get the phenotypes of the current patient node") + public List getPhenotypes() + { + switchToTab("Clinical"); + return getLabelsFromList(phenotypesList); + } + + /** + * Adds the passed phenotypes to the patient. Will select the first suggestion for each phenotype. Requires the + * patient information modal to be open. + * + * @param loPhenotypesToAdd A List containing Strings of the phenotypes to add. Each should be as close as possible + * to the desired phenotype. + * @return Stay on the same page so return the same object. + */ + @Step("Add the following phenotypes to the patient node: {0}") + public PedigreeEditorPage addPhenotypes(List loPhenotypesToAdd) + { + switchToTab("Clinical"); + for (String phenotype : loPhenotypesToAdd) { + clickAndTypeOnElement(phenotypeBox, phenotype); + clickOnElement(linkPatientFirstSuggestion); + } + return this; + } + + /** + * Retrieves a list of entered genes from the "Clinical" tab of the specified status. + * + * @param status is the gene status so it is one of "Candidate", "Confirmed Causal", "Carrier" + * @return a List of Strings, possibly empty, representing the text label for each entered gene. + */ + @Step("Retrieve the genes of this patient {0}") + public List getGenes(String status) + { + switchToTab("Clinical"); + switch (status) { + case "Candidate": + return getLabelsFromList(candidateGeneList); + case "Confirmed Causal": + return getLabelsFromList(causalGeneList); + case "Carrier": + return getLabelsFromList(carrierGeneList); + default: + return getLabelsFromList(candidateGeneList); + } + } + + /** + * Adds a list of Candidate Genes via the Patient Information modal. Will select the first suggestion that pops up. + * Requires the Patient Info modal to be present. + * + * @param loCandidateGenesToAdd A list of Strings, can be empty, of Candidate genes to add. Each should be as close + * as possible to the exact gene name. + * @return Stay on the same page so return the same object. + */ + public PedigreeEditorPage addCandidateGenes(List loCandidateGenesToAdd) + { + switchToTab("Clinical"); + for (String candidateGene : loCandidateGenesToAdd) { + clickAndTypeOnElement(candidateGeneBox, candidateGene); + clickOnElement(linkPatientFirstSuggestion); + } + return this; + } + + /** + * Adds a gene of geneStatus to the patient with its information modal open. Will select the first gene suggestion. + * Requires that the Patient Information modal be present. + * + * @param geneName Name of the gene to add. Should be an exact String to the gene name. + * @param geneStatus Status of the gene, one of: "Candidate", "Confirmed Causal", and "Carrier" + * @return Stay on the same page so return the same object. + */ + @Step("Add gene: {0} with status: {1}") + public PedigreeEditorPage addGene(String geneName, String geneStatus) + { + switchToTab("Clinical"); + switch (geneStatus) { + case "Candidate": + clickAndTypeOnElement(candidateGeneBox, geneName); + break; + case "Confirmed Causal": + clickAndTypeOnElement(causalGeneBox, geneName); + break; + case "Carrier": + clickAndTypeOnElement(carrierGeneBox, geneName); + break; + default: + clickAndTypeOnElement(candidateGeneBox, geneName); + break; + } + clickOnElement(linkPatientFirstSuggestion); + + return this; + } + + /******************************************************** + * Hover boxes and Patient/Family Member Nodes - Methods + ********************************************************/ + /** + * Opens the edit patient modal for the Nth patient it can find on the editor. + * + * @return stay on the same page so return same object. TODO: Figure out how to traverse and search the possible + * nodes for a patient, The js might might make this interesting... Ideas: Differentiate via width and height. + * rect.pedigree-hoverbox[width=180, height=243] for people, rect.pedigree-hoverbox[width=52, height=92] for + * relationship node. + */ + @Step("Open the {0}th node's edit modal") + public PedigreeEditorPage openNthEditModal(int n) + { + waitForElementToBePresent(hoverBox); + unconditionalWaitNs(3); // Figure out how to wait for animation to finish + + System.out.println("Wait for hover box is done - 3 secs. Now find and click."); + + List loHoverBoxes = superDriver.findElements(hoverBox); + + System.out.println("Found hoverboxes: " + loHoverBoxes.size()); + + Actions action = new Actions(superDriver); + action.moveToElement(loHoverBoxes.get(n - 1)) // Wiggles the mouse a little bit + .moveToElement(loHoverBoxes.get(n - 1), 10, 10) + .pause(1000) + .click(loHoverBoxes.get(n - 1)) + .build() + .perform(); + + if (!isElementClickable(personalTab)) { + loHoverBoxes = superDriver.findElements(hoverBox); // Search again, maybe coordiantes changed. + System.out.println("Note: Clicking " + n + "th hover box again..."); + System.out.println("Found hoverboxes, Second Try: " + loHoverBoxes.size()); + action.moveToElement(loHoverBoxes.get(n - 1), 10, 10) + .moveToElement(loHoverBoxes.get(n - 1)) + .pause(1000) + .click(loHoverBoxes.get(n - 1)) + .build() + .perform(); + } + waitForElementToBeClickable(personalTab); + return this; + } + + /** + * Creates a child of the specified gender. Currently, it just does it for the first patient node that it can find. + * + * @param gender is one of the options on the gender toolbar that appears when trying to create a new node "Male", + * "Female", "Unknown", etc. + * @return Stay on the same page so return the same object. + */ + @Step("Create a child of the currently focused node with gender {0}") + public PedigreeEditorPage createChild(String gender) + { + waitForElementToBePresent(hoverBox); + + openNthEditModal(1); + waitForElementToBeClickable(createChildNode); + + List loChildCreateNodes = superDriver.findElements(createChildNode); + loChildCreateNodes.get(1).click(); + forceClickOnElement(createMaleNode); + + return this; + } + + /** + * Returns the number of total hover boxes in the family tree. A hover box can either be a patient node or it can be + * a partnership linkage node. + * + * @return An integer >= 0 representing the number of clickable hover boxes within the tree. + */ + @Step("Get the number of nodes") + public int getNumberOfNodes() + { + waitForElementToBePresent(closeEditor); + return superDriver.findElements(hoverBox).size(); + } + + /** + * Gives the number of partner links within the tree. This node is where the "Consanguinity of this relationship" + * appears in a modal. + * + * @return int >= 0 representing the number of partner links in the tree. + */ + @Step("Get the number of partner links on the tree") + public int getNumberOfPartnerLinks() + { + waitForElementToBePresent(closeEditor); + return superDriver.findElements(hoverBoxPartnerLink).size(); + } + + /** + * Gives the total number of patients, regardless if linked to an existing patient or not, in the tree. I.e. it is + * total number of hover boxes - number of linkage hover boxes. + * + * @return Integer >= 0 representing the total number of patients on tree. + */ + @Step("Get the number of patients found on tree") + public int getNumberOfTotalPatientsInTree() + { + return getNumberOfNodes() - getNumberOfPartnerLinks(); + } + + /** + * Finds a list of Patient IDs that have been linked and are visible on the pedigree editor. + * + * @return A list of Strings, possibly empty, which are the patient IDs for those who are linked to a node on the + * pedigree editor. I.e. a List of Strings in the form of Pxxxxxxx + */ + public List getListOfLinkedPatients() + { + List loPatientIDs = new ArrayList<>(); + waitForElementToBePresent(closeEditor); + superDriver.findElements(linkedPatientIDLink) + .forEach(element -> loPatientIDs.add(element.getText())); + + return loPatientIDs; + } + + /** + * Supposed to create a partnership between two patient nodes by clicking and dragging on the leftmost circle. The + * partners are specified by the Nth hover box they represent on the page. + * + * @param partner1 is the patient node that has the left circle to be clicked and dragged on + * @param partner2 is the patient node that is meant to be dragged to wards + * @return Stay on the same page so return the same object. TODO: I don't think this is working... we really need + * better structure for it to be testable + */ + public PedigreeEditorPage createPartnership(int partner1, int partner2) + { + Actions act = new Actions(superDriver); + + waitForElementToBePresent(hoverBox); + List loHoverBoxes = superDriver.findElements(hoverBox); + + act.moveToElement(loHoverBoxes.get(partner1 - 1)) +// .pause(2000) + .build().perform(); + + forceClickOnElement(createPartnerNode); + waitForElementToBePresent(createPartnerNode); + List loPartnerNodes = superDriver.findElements(createPartnerNode); + + act.dragAndDrop(loPartnerNodes.get(partner1 - 1), + loHoverBoxes.get(partner2 - 1)) + .build() + .perform(); + + return this; + } + + /** + * Creates a sibling for the patient specified by the Nth hover box. Requires that the Nth hover box exists. + * + * @param NthHoverBox is the nth hover box for the patient that we want to create a sibling for. + * @return Stay on the same page, so return the same object. + */ + @Step("Create a sibling for the patient on the {0}th hover box") + public PedigreeEditorPage createSibling(int NthHoverBox) + { + waitForElementToBePresent(hoverBox); + + openNthEditModal(NthHoverBox); + waitForElementToBePresent(createSiblingNode); + + List loChildCreateNodes = superDriver.findElements(createSiblingNode); + List loMaleNodes = superDriver.findElements(createMaleNode); + + System.out.println("DEBUG Create Nodes found: " + loChildCreateNodes.size()); + + clickOnElement(loChildCreateNodes.get(2 * NthHoverBox - 1)); + loMaleNodes.get(1).click(); + + return this; + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/UserSignUpPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/UserSignUpPage.java new file mode 100644 index 00000000..4fc8bd87 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/UserSignUpPage.java @@ -0,0 +1,91 @@ +package org.phenotips.endtoendtests.pageobjects; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import io.qameta.allure.Step; + +/** + * Corresponds to the "Request An Account" page that the public can see to request to sign up. i.e. + * http://localhost:8083/register/PhenomeCentral/WebHome + */ +public class UserSignUpPage extends BasePage implements CommonSignUpSelectors +{ + private final By registerBtn = By.cssSelector("input[type=submit][value=Register]"); + + private final By cancelAndReturnBtn = By.cssSelector("#register a.button.secondary"); + + private final By infoMessageArea = By.cssSelector("div.infomessage"); + + public UserSignUpPage(WebDriver aDriver) + { + super(aDriver); + } + + /** + * Requests an account through the sign up page. Only fills out the fields and submits the request, does not approve + * it. + * + * @param firstName First Name as a String. + * @param lastName Last name as a String. + * @param password is password as a String. + * @param email is email for the user as a String. Should be either a dummy address or something that we can + * access. + * @param affiliation is the value for the Affiliation box as a String. + * @param referral value for the "How did you hear about/ Who referred you" box as a String. + * @param justification value for the "Why are you requesting access" box as a String. + * @return Stay on the same page so return the same object. + */ + @Step("Request an account for {0}. Information should be listed below.") + public UserSignUpPage requestAccount(String firstName, String lastName, String password, String confirmPassword, + String email, String affiliation, String referral, String justification) + { + clickAndClearElement(userNameBox); + unconditionalWaitNs(1); // Needed as entering a firstname immediately does not regen username. + + clickAndTypeOnElement(firstNameBox, firstName); + clickAndTypeOnElement(lastNameBox, lastName); + clickOnElement(userNameBox); + clickAndTypeOnElement(passwordBox, password); + clickAndTypeOnElement(confirmPasswordBox, confirmPassword); + clickAndTypeOnElement(emailBox, email); + clickAndTypeOnElement(affiliationBox, affiliation); + clickAndTypeOnElement(referralBox, referral); + clickAndTypeOnElement(reasoningBox, justification); + + toggleCheckboxToChecked(professionalCheckbox); + toggleCheckboxToChecked(liabilityCheckbox); + toggleCheckboxToChecked(nonIdentificationCheckbox); + toggleCheckboxToChecked(cooperationCheckbox); + toggleCheckboxToChecked(acknoledgementCheckbox); + + clickOnElement(registerBtn); + return this; + } + + /** + * Retrieves the confirmation message that is presented to the user when submitting sign up information. As of + * writing this code, it says "Thank you for your interest in PhenomeCentral. We took note of your request and we + * will process it shortly." + * + * @return A String representing the message. + */ + @Step("Retreieve the confirmation message that account has been requested") + public String getConfirmationMessage() + { + waitForElementToBePresent(infoMessageArea); + return superDriver.findElement(infoMessageArea).getText(); + } + + /** + * Cancel the Request an Account page. Navigates back to the home page by clicking on the cancel button + * + * @return Navigate back to the home page so return a new instance of HomePage object. + */ + @Step("Cancel the account request going back to the home page.") + public HomePage cancelRequestingAccount() + { + clickOnElement(cancelAndReturnBtn); + return new HomePage(superDriver); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/ViewPatientPage.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/ViewPatientPage.java new file mode 100644 index 00000000..9e1db5e0 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/pageobjects/ViewPatientPage.java @@ -0,0 +1,193 @@ +package org.phenotips.endtoendtests.pageobjects; + +import java.util.List; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import io.qameta.allure.Step; + +/** + * Represents viewing a specifc patient's full information page. Ex. http://localhost:8083/P0000005 + */ +public class ViewPatientPage extends CommonInfoSelectors +{ + private final By patientID = By.cssSelector("#document-title > h1:nth-child(1)"); + + private final By editBtn = By.id("prActionEdit"); + + private final By similarityTable = By.cssSelector(".similarity-results"); + + private final By geneNames = + By.cssSelector("#extradata-list-PhenoTips\\2e GeneClass_PhenoTips\\2e GeneVariantClass td.Gene > p"); + + private final By geneStatuses = + By.cssSelector("#extradata-list-PhenoTips\\2e GeneClass_PhenoTips\\2e GeneVariantClass td.Status"); + + private final By geneStrategies = + By.cssSelector("#extradata-list-PhenoTips\\2e GeneClass_PhenoTips\\2e GeneVariantClass td.Strategy"); + + private final By geneComments = + By.cssSelector("#extradata-list-PhenoTips\\2e GeneClass_PhenoTips\\2e GeneVariantClass td.Comments"); + + private final By clinicalDiagnosisNames = + By.cssSelector("div.diagnosis-info div.clinical_diagnosis div.vocabulary-term-list > p"); + + private final By finalDiagnosisNames = + By.cssSelector("div.diagnosis-info div.omim_id div.vocabulary-term-list > p"); + + private final By additionalCommentsText = By.cssSelector("div.diagnosis_notes > div.displayed-value > p"); + + private final By pubMedIDsPresent = By.cssSelector("div.article-ids > span.p-id:nth-child(1)"); + + private final By resolutionNotesText = By.cssSelector("div.solved__notes > div > p"); + + public ViewPatientPage(WebDriver aDriver) + { + super(aDriver); + } + + /** + * Returns a string representing the current patient's ID number. It is the string at the top left corner of the + * page. + * + * @return a String in the form of Pxxxxxxx + */ + @Step("Retrieve the patient ID") + public String getPatientID() + { + waitForElementToBePresent(patientID); + return superDriver.findElement(patientID).getText(); + } + + /** + * Clicks on the "Edit" link to edit the patient + * + * @return new patient editor page object as we navigate to the patient editing page + */ + @Step("Edit the currently viewed patient") + public CreatePatientPage editThisPatient() + { + clickOnElement(editBtn); + return new CreatePatientPage(superDriver); + } + + /** + * Retrieves all the gene names found in the "Genotype information" section. The order of the table is preserved. + * + * @return A, possibly empty, list of strings containing the gene names found. This should not have empty strings + * (i.e. ""). + */ + @Step("Retrieve the gene names") + public List getGeneNames() + { + return getLabelsFromList(geneNames); + } + + /** + * Retrieves all the gene statuses found in the "Genotype information" section. The order of the column is + * preserved. + * + * @return A, possibly empty, list of strings containing the gene statuses found (the entire column). This might + * have empty strings (i.e. "") for genes with unspecified status. + */ + @Step("Retrieve gene statuses") + public List getGeneStatus() + { + return getLabelsFromList(geneStatuses); + } + + /** + * Retrieves all the gene strategies found in the "Genotype information" section. The order of the column is + * preserved. + * + * @return A, possibly empty, list of strings containing the gene strategies found. This might have empty strings + * (i.e. "") for genes with unspecified strategy. + */ + @Step("Retrieve gene strategies") + public List getGeneStrategies() + { + return getLabelsFromList(geneStrategies); + } + + /** + * Retrieves all the gene comments found in the "Genotype information" section. The order of the column is + * preserved. + * + * @return A, possibly empty, list of strings containing the gene strategies found. This might have empty strings + * (i.e. "") for genes with no comments. + */ + @Step("Retrieve gene comments") + public List getGeneComments() + { + return getLabelsFromList(geneComments); + } + + /** + * Retrieves all the ORDO names of Clinical Diagnosis, in order, that have been entered within the "Diagnosis" + * section. Each String is ORDO number followed by name. Ex. "427 Familial hypoaldosteronism". + * + * @return A possibly empty, list of Strings representing the ORDO names that were found under Clinical Diagnosis. + */ + @Step("Retrieve the clinical diagnosis names") + public List getClinicalDiagnosisNames() + { + return getLabelsFromList(clinicalDiagnosisNames); + } + + /** + * Retrieves all the OMIM names of Final Diagnosis, in order, that have been entered within the "Diagnosis" section. + * Each String has OMIM number and name. Ex. "#151623 LI-FRAUMENI SYNDROME" + * + * @return A possibly empty, list of Strings representing the OMIM names that were found under Final Diagnosis. + */ + @Step("Retrieve the final diagnosis names") + public List getFinalDiagnosisNames() + { + return getLabelsFromList(finalDiagnosisNames); + } + + /** + * Retrieves the additional comment under the Diagnosis section. + * + * @return A String containing the comment under Additional Comments. If there is no Additional Comments section, + * will return an empty String (""). + */ + @Step("Retrieve any additional comments") + public String getAdditionalComments() + { + if (isElementPresent(additionalCommentsText)) { + return superDriver.findElement(additionalCommentsText).getText(); + } else { + return ""; + } + } + + /** + * Retrieves a List of PubMed IDs that have been entered in the "This case is published in:" area under the + * "Diagnosis" section. These strings will be in the form of "PMID: 30700910". + * + * @return A, possibly empty, list of Strings containing the PMIDs of entered cases. + */ + @Step("Get existing PubMedIDs") + public List getExistingPubMedIDs() + { + return getLabelsFromList(pubMedIDsPresent); + } + + /** + * Retrieves the comment under the "Resolution Notes:" area under the "Diagnosis" section. If there is no note and + * the element is not visible, will return empty String (""). + * + * @return A String representing the contents of what was entered and saved in the "Resolution Notes" box. + */ + @Step("Retrieve the resolution notes for the patient") + public String getResolutionNotes() + { + if (isElementPresent(resolutionNotesText)) { + return superDriver.findElement(resolutionNotesText).getText(); + } else { + return ""; + } + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/AddingUsersTests.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/AddingUsersTests.java new file mode 100644 index 00000000..0964854f --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/AddingUsersTests.java @@ -0,0 +1,186 @@ +package org.phenotips.endtoendtests.testcases; + +import org.phenotips.endtoendtests.pageobjects.HomePage; +import org.phenotips.endtoendtests.pageobjects.UserSignUpPage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import net.bytebuddy.utility.RandomString; + +/** + * Test cases for adding users to the instance. These tests can be run individually, or as a class. There are functional + * tests for ensuring the user approval workflow and ensuring that the input fields on the "Request Account" form have + * error checking. + */ +public class AddingUsersTests extends BaseTest +{ + final HomePage aHomePage = new HomePage(theDriver); + + final UserSignUpPage aUserSignUpPage = new UserSignUpPage(theDriver); + + // Strings of messages to verify: + + final String confirmationMessageCheck = + "Thank you for your interest in PhenomeCentral. We took note of your request and we will process it shortly."; + + final String pendingApprovalMessageCheck = "Please wait for your account to be approved. Thank you."; + + final String randomChars = RandomString.make(5); + + // Common Strings for creation of patients: + + final String password = "123456"; + + final String affiliation = "someaffilation"; + + final String referrer = "some referrer"; + + final String justification = "some reason"; + + List loSectionTitlesCheck = new ArrayList<>(Arrays.asList("MY MATCHES", "MY PATIENTS\n ", + "PATIENTS SHARED WITH ME\n ", "MY GROUPS\n ", "PUBLIC DATA\n ")); + + // Adds a user through the admin's Users page. Approve the user and login and ensure that dashboard widgets (i.e. + // My Patients, My Families, etc. are visible. + @Test + public void adminAddAndApproveUser() + { + aHomePage.navigateToLoginPage().loginAsAdmin() + .navigateToAdminSettingsPage().navigateToAdminUsersPage() + .addUser("AutoAdded1" + randomChars, "AutoAdded1Last", password, + "AutoAdded1" + randomChars + "@somethingsomething.cjasdfj", affiliation, referrer, justification) + .navigateToPendingUsersPage() + .approveNthPendingUser(1); + + aHomePage.logOut() + .loginAs("AutoAdded1" + randomChars + "AutoAdded1Last", password) + .navigateToHomePage(); + + Assert.assertEquals(aHomePage.getSectionTitles(), loSectionTitlesCheck); + + aHomePage.logOut(); + } + + // User signs up and is pending approval. Asserts that the user sees the Pending Approval message on a few pages. + @Test + public void userSignUpNotApproved() + { + final String firstname = "PublicSignUp" + randomChars; + final String lastname = "Auto" + randomChars; + + aHomePage.navigateToSignUpPage() + .requestAccount(firstname, lastname, password, password, + firstname + "@akjsjdf.cjsjdfn", affiliation, referrer, justification); + + Assert.assertEquals(aUserSignUpPage.getConfirmationMessage(), confirmationMessageCheck); + System.out.println("Request Recieved Msg: " + aUserSignUpPage.getConfirmationMessage()); + + aHomePage.navigateToLoginPage() + .loginAs(firstname + lastname, password); + + System.out.println("Approval Pending Msg: " + aHomePage.getApprovalPendingMessage()); + Assert.assertEquals(aHomePage.getApprovalPendingMessage(), pendingApprovalMessageCheck); + Assert.assertEquals(aHomePage.navigateToAllPatientsPage().getApprovalPendingMessage(), + pendingApprovalMessageCheck); + Assert.assertEquals(aHomePage.navigateToCreateANewPatientPage().getApprovalPendingMessage(), + pendingApprovalMessageCheck); + + aHomePage.logOut(); + } + + // User signs up via public "Request An Account" form, approve account, and then login to ensure that dashboard widgets + // Ex. Patients, Families sections are visible. + @Test + public void userSignUpApproved() + { + String firstName = "PublicSignUpApproved" + randomChars; + String lastName = "Auto" + randomChars; + String username = firstName + lastName; + + aHomePage.navigateToSignUpPage() + .requestAccount(firstName, lastName, password, password, + firstName + "@akjsjdf.cjsjdfn", affiliation, referrer, justification); + + Assert.assertEquals(aUserSignUpPage.getConfirmationMessage(), confirmationMessageCheck); + + aHomePage.navigateToLoginPage() + .loginAs(username, password); + + System.out.println("Approval Pending Msg: " + aHomePage.getApprovalPendingMessage()); + Assert.assertEquals(aHomePage.getApprovalPendingMessage(), pendingApprovalMessageCheck); + + aHomePage.logOut(); + aHomePage.navigateToLoginPage() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToPendingUsersPage() + .approvePendingUser(username) + .logOut() + .loginAs(username, password) + .navigateToHomePage(); + + System.out.println("Titles Found: " + aHomePage.getSectionTitles()); + Assert.assertEquals(aHomePage.getSectionTitles(), loSectionTitlesCheck); + } + + // Error check the input fields, ensures that a request is not sent if any one of the required fields are missing or invalid. + // This test keeps trying to create the user on the same request form, so it will fail if there was no error check and we + // navigate away from that page. + @Test + public void errorCheckFields() + { + aHomePage.navigateToSignUpPage() + .requestAccount("PublicDupe" + randomChars, "Duped" + randomChars, "123456", "123456", + "PublicSignUpDupe" + randomChars + "@something.something", "none", + "Test server", "Some reason"); + + // Duplicate Username + aHomePage.navigateToHomePage() + .navigateToSignUpPage() + .requestAccount("PublicDupe" + randomChars, "Duped" + randomChars, "123456", "123456", + "PublicSignUpDupe" + randomChars + "@something.something", "none", + "Test server", "Some reason") + + // Mismatching passwords + .requestAccount("MisMatched PassWord" + randomChars, "Mismatch", "123456", "123456 ", + "PublicSignUpDupe" + randomChars + "@something.something", "none", + "Test server", "Some reason") + + // Invalid Email + .requestAccount("Invalid Email" + randomChars, "Invalidated", "123456", "123456", + "PublicSignUpDupe" + randomChars + "@NoExtension", "none", + "Test server", "Some reason") + + // Lacking first name + .requestAccount("", "First Name Missing", "123456", "123456", + "PublicSignUpDupe" + randomChars + "@something.something", "none", + "Test server", "Some reason") + + // Lacking last name + .requestAccount("LastName Missing", "", "123456", "123456", + "PublicSignUpDupe" + randomChars + "@something.something", "none", + "Test server", "Some reason") + + // Lacking password + .requestAccount("MisMatched PassWord" + randomChars, "Mismatch", "", "", + "PublicSignUpDupe" + randomChars + "@something.something", "none", + "Test server", "Some reason") + + // Lacking email + .requestAccount("Email Missing", "Missings", "123456", "123456", + "", "none", + "Test server", "Some reason") + + // Lack of Affiliation + .requestAccount("No Affiliation" + randomChars, "Reasoning", "123456", "123456", + "PublicSignUpDupe" + randomChars + "@something.something", "", + "Test server", "Some reason") + .cancelRequestingAccount() + .navigateToLoginPage(); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/BaseTest.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/BaseTest.java new file mode 100644 index 00000000..9c0e75a7 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/BaseTest.java @@ -0,0 +1,217 @@ +package org.phenotips.endtoendtests.testcases; + +import org.phenotips.endtoendtests.pageobjects.HomePage; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; + +import org.apache.commons.io.FileUtils; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.UnhandledAlertException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.edge.EdgeDriver; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.ie.InternetExplorerDriver; +import org.openqa.selenium.safari.SafariDriver; +import org.testng.ITestResult; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.AfterSuite; + +import io.github.bonigarcia.wdm.WebDriverManager; +import io.qameta.allure.Allure; +import io.qameta.allure.Step; + +/** + * An abstract test. All tests should inherit this class. We should put any high level methods using @after- annotations + * here We can also put any high level @BeforeSuite methods here too to setup/check main conditions. + */ +public abstract class BaseTest +{ + protected static WebDriver theDriver; + + /** + * Instantiate the webDriver instance here. The WebDriverManager takes care of setting up the environment including + * the intermediary protocol used to communicate with the browser. This allows a user to just run the tests and the + * manager should take care of finding the path to the desired executable and setting up environment. + */ + public BaseTest() + { + if (theDriver == null) { + printCommandLinePropertyMessages(); + setUpBrowser(); + } + } + + /** + * Runs after every test method. In the case that TestNG's listener reports a failure, call methods to take a + * screenshot and to cleanup the state of the browser. + * + * @param testResult resulting status of a test method that has just run, as reported by TestNGs listener. Check + * this passed info for failure. + */ + @AfterMethod + public void onTestEnd(ITestResult testResult) + { + String testMethod = testResult.getMethod().getMethodName(); + + if (ITestResult.FAILURE == testResult.getStatus()) { + System.out.println("Test:" + testMethod + " has failed. Taking a screenshot and cleaning up..."); + captureScreenshot(testMethod, theDriver.getCurrentUrl()); + cleanupBrowserState(); + } else if (ITestResult.SKIP == testResult.getStatus()) { + System.out.println("Test:" + testMethod + + " was skipped, possibly due to unfinished dependent tests, system error, or unable to reach Selenium. No screenshot. Moving on."); + } else { + System.out.println("Test:" + testMethod + " has succeeded. No screenshot. Moving on."); + } + } + + /** + * Runs after each class finishes. Now no longer really being used, except for a debug message. + */ + @AfterClass + public void testCleanup() + { + System.out.println("A single class has finished"); + } + + /** + * Runs after the entire suite (all test cases specified in the test run) are finished. We explicitly quit the + * webDriver here as it does not close on its own when the reference is trashed. Quitting the webDriver means + * closing the browser window and quitting the firefox process (important). + */ + @AfterSuite + public void cleanup() + { + System.out.println("Test suite finished running"); + + if (theDriver != null) { + theDriver.quit(); + } + } + + /** + * Helper function called in ctor to instantiate the desired browser. See BaseTest ctor. + */ + private void setUpBrowser() + { + String browser = System.getProperty("browser", "chrome"); // If null, set to Chrome + + if (browser.equalsIgnoreCase("chrome")) { + WebDriverManager.chromedriver().setup(); + theDriver = new ChromeDriver(); + } else if (browser.equalsIgnoreCase("firefox")) { + WebDriverManager.firefoxdriver().setup(); + theDriver = new FirefoxDriver(); + } else if (browser.equalsIgnoreCase("edge")) { + WebDriverManager.edgedriver().setup(); + theDriver = new EdgeDriver(); + } else if (browser.equalsIgnoreCase("ie")) { + WebDriverManager.iedriver().setup(); + theDriver = new InternetExplorerDriver(); + } else if (browser.equalsIgnoreCase("safari")) { + // No need to setup for Safari, native Selenium api should work... according to Apple docs + theDriver = new SafariDriver(); + } else { + System.out.println("Unknown browser, defaulting to Chrome. browser can be one of (case insensitive): "); + System.out.println("chrome, firefox, edge, ie, safari"); + WebDriverManager.chromedriver().setup(); + theDriver = new ChromeDriver(); + } + + System.out.println("Initiated the webDriver for: " + browser); + } + + /** + * Prints out debug messages for the three environment variables (the ones the tests are interested in) that were + * passed through the command line. This includes the homePageURL, emailUIPageURL and browser if they were passed. + * TODO: Perhaps create a superclass for BasePage/BaseTest that handles command line parameters in one place, rather + * than have the URLs set in BasePage and the browser set in BaseTest + */ + private void printCommandLinePropertyMessages() + { + // Detect if any command line parameters were passed to specify browser, homepageURL or + // EmailUI page URL + if (System.getProperty("homePageURL") != null) { + System.out.println("homePageURL property passed. PC instance is at: " + System.getProperty("homePageURL")); + } + + if (System.getProperty("emailUIPageURL") != null) { + System.out + .println("emailUIPageURL property passed. EmailUI is at: " + System.getProperty("emailUIPageURL")); + } + + if (System.getProperty("browser") != null) { + System.out.println("browser property passed. Browser to use: " + System.getProperty("browser")); + } + } + + /** + * Captures a screenshot of the browser viewport as a PNG to the target/screenshot folder. This is called by the + * AfterMethod in the case of a test failure. + * + * @param testMethod The method name of the test that failed, as a String. Will be used to name the file. + * @param URL the URL that the browser was at during a test failure. Used by Allure's listener in the Step + * annotation + */ + @Step("Taking screenshot of {0} for URL {1}") + private void captureScreenshot(String testMethod, String URL) + { + // Screenshot mechanism + // Cast webDriver over to TakeScreenshot. Call getScreenshotAs method to create image file + File srcFile = ((TakesScreenshot) theDriver).getScreenshotAs(OutputType.FILE); + + LocalDateTime dateTime = ZonedDateTime.now().toLocalDateTime(); + + String scrnFileName = "target/screenshots/" + testMethod + "_" + dateTime + ".png"; + + // Save screenshot in target/screenshots folder with the methodName of failed test and timestamp. + File destFile = new File(scrnFileName); + + System.out.println("Taking screenshot of failed test..."); + + // Copy over to target/screenshots folder + try { + FileUtils.copyFile(srcFile, destFile); + } catch (IOException e) { + System.out.println("Something went wrong copying over screenshot to target/screenshots: " + e); + } + + // Add screenshot to Allure Report + Path content = Paths.get(scrnFileName); + try (InputStream is = Files.newInputStream(content)) { + Allure.addAttachment("Page Screenshot: " + testMethod, is); + } catch (IOException e) { + System.out.println("Something went wrong giving screenshot to Allure: " + e); + } + } + + /** + * Cleans up the state of the browser after a test failure. This takes care of any warning modal that pops up for + * unsaved edits before navigating back to the login page so that the next tests can continue. Called by the + * AfterMethod in case of test failure. + */ + private void cleanupBrowserState() + { + HomePage tempHomePage = new HomePage(theDriver); + + try { + tempHomePage.navigateToLoginPage(); + System.out.println("Test failure, navigate to login page. There is no unsaved changes warning."); + } catch (UnhandledAlertException e) { + theDriver.switchTo().alert().accept(); + theDriver.switchTo().defaultContent(); + tempHomePage.navigateToLoginPage(); + System.out.println("Test failure, navigate to login page. Closed an unsaved changes warning dialogue"); + } + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/CreatePatientTest.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/CreatePatientTest.java new file mode 100644 index 00000000..95427178 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/CreatePatientTest.java @@ -0,0 +1,361 @@ +package org.phenotips.endtoendtests.testcases; + +import org.phenotips.endtoendtests.common.CommonInfoEnums; +import org.phenotips.endtoendtests.common.CommonPatientMeasurement; +import org.phenotips.endtoendtests.pageobjects.AdminRefreshMatchesPage; +import org.phenotips.endtoendtests.pageobjects.CreatePatientPage; +import org.phenotips.endtoendtests.pageobjects.EmailUIPage; +import org.phenotips.endtoendtests.pageobjects.HomePage; +import org.phenotips.endtoendtests.pageobjects.ViewPatientPage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import net.bytebuddy.utility.RandomString; + +/** + * Testing the creation of two very similar patients via JSON import and manually. Asserts a match at end. + * + * The entire class should be run together. We need the first two methods to be run first to have two patients created. + * + * The remaining tests depend on them. Requires MockMock email SMTP service to be running for it to check emails. + */ +public class CreatePatientTest extends BaseTest implements CommonInfoEnums +{ + final private HomePage aHomePage = new HomePage(theDriver); + + final private ViewPatientPage aViewPatientPage = new ViewPatientPage(theDriver); + + final private CreatePatientPage aCreatePatientPage = new CreatePatientPage(theDriver); + + final private SECTIONS[] checkForTheseSections = { + SECTIONS.PatientInfoSection, + SECTIONS.ClinicalSymptomsSection, + SECTIONS.SuggestedGenesSection, + SECTIONS.GenotypeInfoSection, + SECTIONS.SimilarCasesSection + }; + + final private String randomChars = RandomString.make(5); + + final private String patientUniqueIdentifier = "Auto " + randomChars + " Patient"; + + final private String JSONToImport = + "[{\"allergies\":[],\"date\":\"2019-01-11T17:26:01.000Z\",\"apgar\":{},\"notes\":{\"family_history\":\"\",\"prenatal_development\":\"\",\"indication_for_referral\":\"\",\"genetic_notes\":\"\",\"medical_history\":\"\",\"diagnosis_notes\":\"\"},\"ethnicity\":{\"maternal_ethnicity\":[],\"paternal_ethnicity\":[]},\"date_of_birth\":{\"month\":3,\"year\":2011},\"global_mode_of_inheritance\":[],\"solved\":{\"status\":\"unsolved\"},\"external_id\":\"" + + patientUniqueIdentifier + + "\",\"variants\":[],\"clinicalStatus\":\"affected\",\"disorders\":[],\"features\":[{\"id\":\"HP:0000385\",\"label\":\"Small earlobe\",\"type\":\"phenotype\",\"observed\":\"yes\"},{\"id\":\"HP:0000505\",\"label\":\"Visual impairment\",\"type\":\"phenotype\",\"observed\":\"yes\"},{\"id\":\"HP:0000618\",\"label\":\"Blindness\",\"type\":\"phenotype\",\"observed\":\"yes\"},{\"id\":\"HP:0001250\",\"label\":\"Seizures\",\"type\":\"phenotype\",\"observed\":\"yes\"},{\"id\":\"HP:0002121\",\"label\":\"Absence seizures\",\"type\":\"phenotype\",\"observed\":\"yes\"},{\"id\":\"HP:0006266\",\"label\":\"Small placenta\",\"type\":\"phenotype\",\"observed\":\"yes\"},{\"id\":\"HP:0007875\",\"label\":\"Congenital blindness\",\"type\":\"phenotype\",\"observed\":\"yes\"},{\"id\":\"HP:0011146\",\"label\":\"Dialeptic seizures\",\"type\":\"phenotype\",\"observed\":\"yes\"},{\"id\":\"HP:0011147\",\"label\":\"Typical absence seizures\",\"type\":\"phenotype\",\"observed\":\"yes\"},{\"id\":\"HP:0200055\",\"label\":\"Small hand\",\"type\":\"phenotype\",\"observed\":\"yes\"}],\"date_of_death\":\"\",\"last_modification_date\":\"2019-01-11T17:31:13.000Z\",\"nonstandard_features\":[],\"prenatal_perinatal_history\":{\"multipleGestation\":null,\"icsi\":null,\"ivf\":null,\"assistedReproduction_donoregg\":null,\"assistedReproduction_iui\":null,\"twinNumber\":\"\",\"assistedReproduction_fertilityMeds\":null,\"gestation\":null,\"assistedReproduction_surrogacy\":null,\"assistedReproduction_donorsperm\":null},\"family_history\":{\"miscarriages\":null,\"consanguinity\":null,\"affectedRelatives\":null},\"genes\":[{\"gene\":\"PLS1\",\"id\":\"ENSG00000120756\",\"strategy\":[\"sequencing\"],\"status\":\"candidate\"},{\"gene\":\"PLS3\",\"id\":\"ENSG00000102024\",\"strategy\":[\"sequencing\"],\"status\":\"candidate\"},{\"gene\":\"QSOX1\",\"id\":\"ENSG00000116260\",\"strategy\":[\"sequencing\"],\"status\":\"solved\"},{\"gene\":\"TXNL1\",\"id\":\"ENSG00000091164\",\"strategy\":[\"sequencing\"],\"status\":\"carrier\"}],\"life_status\":\"alive\",\"sex\":\"M\",\"clinical-diagnosis\":[],\"reporter\":\"TestUser1Uno\",\"last_modified_by\":\"TestUser1Uno\",\"global_age_of_onset\":[{\"id\":\"HP:0003577\",\"label\":\"Congenital onset\"}],\"report_id\":\"P0000009\",\"medical_reports\":[]}\n" + + "]"; + + // This is a helper test to ensure matches are refreshed before any of the following tests are run. + @Test + public void initialMatchesRefresh() + { + aHomePage.navigateToLoginPage() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToRefreshMatchesPage() + .refreshMatchesSinceLastUpdate() + .logOut(); + } + + // Create a patient manually as User 1. Assert that the required section titles in checkForTheseSections are visible. + @Test() + public void createPatientManually() + { + List loPhenotypesToAdd = new ArrayList(Arrays.asList( + "Blindness", "Visual impairment", "Small earlobe", "Small hand", "Absence seizures", + "Diapleptic Seizures", "Typical absence seizures", "Seizures", "Small placenta")); + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToCreateANewPatientPage() + .toggleFirstFourConsentBoxes() + .updateConsent() + .setIdentifer(patientUniqueIdentifier) + .setDOB("02", "2012") + .setGender("Male") + .setOnset("Congenital onset ") + .expandSection(SECTIONS.ClinicalSymptomsSection) + .addPhenotypes(loPhenotypesToAdd) + .expandSection(SECTIONS.ClinicalSymptomsSection) + .expandSection(SECTIONS.GenotypeInfoSection) + .addGene("PLS1", "Candidate", "Sequencing") + .addGene("PLS3", "Candidate", "Sequencing") + .addGene("QSOX1", "Confirmed causal", "Sequencing") + .addGene("TXNL1", "Carrier", "Sequencing") + .saveAndViewSummary(); + + System.out.println("We just edited: " + aViewPatientPage.getPatientID()); + Assert.assertTrue(aViewPatientPage.checkForVisibleSections(checkForTheseSections)); + + aViewPatientPage.logOut(); + } + + // Creates an identical patient as User 2 via JSON import. Asserts that the section titles are visible. + // Updates consent, and changes modifies the identifier so that it is unique and matchable. + @Test() + public void importSecondJSONPatient() + { + aHomePage.navigateToLoginPage() + .loginAsUserTwo() + .navigateToAllPatientsPage() + .importJSONPatient(JSONToImport) + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .toggleFirstFourConsentBoxes() + .updateConsent() + .setIdentifer(patientUniqueIdentifier + " Match") + .saveAndViewSummary(); + + System.out.println("We just edited: " + aViewPatientPage.getPatientID()); + Assert.assertTrue(aViewPatientPage.checkForVisibleSections(checkForTheseSections)); + + aHomePage.logOut(); + } + + // Refresh the matches and assert that two new matches are found. +// @Test(dependsOnMethods = {"createPatientManually", "importSecondJSONPatient"}) +// dependsOnMethods causes order on XML to be ignored as they will have higher priority, +// which is a global variable. + @Test() + public void refreshMatchesForTwoPatients() + { + AdminRefreshMatchesPage aRefreshMatchesPage = new AdminRefreshMatchesPage(theDriver); + + aHomePage.navigateToLoginPage() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToRefreshMatchesPage() + .refreshMatchesSinceLastUpdate(); + + int initialRefreshMatchCount = Integer.parseInt(aRefreshMatchesPage.getTotalMatchesFound()); + String expectedMatchCount = Integer.toString(initialRefreshMatchCount + 2); + + Assert.assertEquals(aRefreshMatchesPage.getNumberOfLocalPatientsProcessed(), "2"); + Assert.assertEquals(aRefreshMatchesPage.getTotalMatchesFound(), expectedMatchCount); + + aHomePage.logOut(); + } + + // Sends the email notification of an identical (100%) match to the two newly created + // patients, checks that the inbox has emails. +// @Test(, dependsOnMethods = {"createPatientManually", "importSecondJSONPatient"}) + @Test() + public void verifyEmailNotifications() + { + EmailUIPage emailPage = new EmailUIPage(theDriver); + + aViewPatientPage.navigateToEmailInboxPage() + .deleteAllEmails(); + + aHomePage.navigateToLoginPage() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToMatchingNotificationPage() + .filterByID(patientUniqueIdentifier + " Match") + .emailFirstRowUsers() + .navigateToEmailInboxPage(); + Assert.assertEquals(emailPage.getNumberOfEmails(), 2); + + emailPage.deleteAllEmails(); + emailPage.navigateToHomePage(); + aViewPatientPage.logOut(); + } + + // Adjusts Patient created by User 1 to public, ensures User 2 can now see it. +// @Test(, dependsOnMethods = {"createPatientManually", "importSecondJSONPatient"}) + @Test() + public void publicVisiblityTest() + { + aHomePage.navigateToLoginPage() + .loginAsUserTwo() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable(); + + String ID1 = aViewPatientPage.getPatientID(); + + aViewPatientPage.setGlobalVisibility("public"); + + aViewPatientPage.logOut() + .loginAsUser() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable(); + + String ID2 = aViewPatientPage.getPatientID(); + + Assert.assertEquals(ID1, ID2); + + aViewPatientPage.logOut().loginAsUserTwo(); + aViewPatientPage.setGlobalVisibility("matchable"); // Set patient back to private to allow for other tests. + aHomePage.logOut(); + } + + // Adds a collaborator to a patient belong to User 2. Asserts that User 1 can then access it. +// @Test(, dependsOnMethods = {"createPatientManually", "importSecondJSONPatient"}) + @Test() + public void collaboratorVisibilityTest() + { + aHomePage + .navigateToLoginPage() + .loginAsUser() + .navigateToAllPatientsPage() + .viewFirstPatientInTable() + .addCollaboratorToPatient("TestUser2Dos", PRIVILAGE.CanViewAndModifyAndManageRights); + + String patientIDThroughUser1 = aViewPatientPage.getPatientID(); + + aViewPatientPage.logOut() + .loginAsUserTwo() + .navigateToAllPatientsPage() + .filterByPatientID(patientIDThroughUser1) + .viewFirstPatientInTable(); + + String patientIDThroughUser2 = aViewPatientPage.getPatientID(); + + Assert.assertEquals(patientIDThroughUser1, patientIDThroughUser2); + + aViewPatientPage.removeNthCollaborator(1); // Remove the collaborator to reset the state. + + aViewPatientPage.logOut(); + } + + // Deletes all patients, helper test in case we want to clean up all patients after this class in the future. + @Test(enabled = false) + public void deleteAllUsersHelper() + { + aHomePage + .navigateToLoginPage() + .loginAsAdmin() + .navigateToAllPatientsPage() + .deleteAllPatients(); + } + + // Adds measurements to User 1's patient, ensures that they are saved and viewable on the view patient form. + @Test() + public void addMeasurements() + { + CommonPatientMeasurement measurements = new CommonPatientMeasurement( + 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, + 16, 17, 18); + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToCreateANewPatientPage() + .toggleFirstFourConsentBoxes() + .updateConsent() + .expandSection(SECTIONS.MeasurementSection) + .addMeasurement(measurements) + .changeMeasurementDate("11", "March", "2015") + .saveAndViewSummary() + .editThisPatient() + .expandSection(SECTIONS.MeasurementSection); + + CommonPatientMeasurement foundMeasurementOnPatientForm = aCreatePatientPage.getPatientMeasurement(); + System.out.println(foundMeasurementOnPatientForm); + Assert.assertEquals(foundMeasurementOnPatientForm, measurements); + + aCreatePatientPage + .saveAndViewSummary() + .logOut(); + } + + // Adds a custom phenotype and then asserts that the automatically added phenotypes show up with the + // lightning bolt symbol and the manually added phenotype doesn't have that symbol. + // DependsOn the addMeasurements() test to have completed. + @Test() + public void checkPhenotypesDueToMeasurements() + { + final List automaticallyAddedPhenotypesToCheck = new ArrayList<>( + Arrays.asList("Decreased body weight", "Short stature", "Microcephaly", + "Obesity", "Long philtrum", "Long palpebral fissure", + "Hypertelorism", "Macrotia", "Small hand", "Short foot")); + + final List manuallyAddedPhenotypesToCheck = new ArrayList<>(Arrays.asList("Blue irides")); + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .setDOB("10", "1992") + .expandSection(SECTIONS.ClinicalSymptomsSection) + .addPhenotype("Blue irides"); + + List automaticallyAddedPhenotypesFound = aCreatePatientPage.getPhenotypesLightning(); + System.out.println(automaticallyAddedPhenotypesFound); + + List manuallyAddedPhenotypesFound = aCreatePatientPage.getPhenotypesNonLightning(); + System.out.println(manuallyAddedPhenotypesFound); + + Assert.assertEquals(automaticallyAddedPhenotypesFound, automaticallyAddedPhenotypesToCheck); + Assert.assertEquals(manuallyAddedPhenotypesFound, manuallyAddedPhenotypesToCheck); + + aCreatePatientPage + .saveAndViewSummary() + .logOut(); + } + + // Enters information to the diagnosis form. Asserts that the diagnosis section has the appropriate fields + // as seen on the view patient form. + @Test() + public void checkDiagnosisSection() + { + final List clinicalDiagnosisCheck = new ArrayList<>( + Arrays.asList("1164 Allergic bronchopulmonary aspergillosis", "52530 Pseudo-von Willebrand disease")); + + final List finalDiagnosisCheck = new ArrayList<>( + Arrays.asList("#607154 ALLERGIC RHINITIS", "#304340 PETTIGREW SYNDROME")); + + final String additionalCommentCheck = "Comment in Additional Comments"; + + final String resolutionNoteCheck = "Resolved"; + + // This array will be sorted as PMIDs do not load in deterministic order. + List pubMedIDsCheck = new ArrayList<>( + Arrays.asList("PMID: 30700955", "PMID: 30699054", "PMID: 30699052")); + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .expandSection(SECTIONS.DiagnosisSection) + .cycleThroughDiagnosisBoxes() + .addClinicalDiagnosis("Pseudo-von Willebrand disease") + .addFinalDiagnosis("PETTIGREW SYNDROME") + .toggleNthFinalDiagnosisCheckbox(2) + .toggleNthClinicalDiagnosisCheckbox(2) + .addPubMedID("30700955") + .saveAndViewSummary(); + + System.out.println("Clinical Diagnosis Found: " + aViewPatientPage.getClinicalDiagnosisNames()); + System.out.println("Final Diagnosis Found: " + aViewPatientPage.getFinalDiagnosisNames()); + System.out.println("Additional Comments Found: " + aViewPatientPage.getAdditionalComments()); + System.out.println("PubMed IDs Found: " + aViewPatientPage.getExistingPubMedIDs()); + System.out.println("Resolution Notes Found: " + aViewPatientPage.getResolutionNotes()); + + // Sort the PubMed IDs as they do not load/display in a deterministic order + List sortedPubMedIDsFound = aViewPatientPage.getExistingPubMedIDs(); + sortedPubMedIDsFound.sort(String::compareTo); + pubMedIDsCheck.sort(String::compareTo); + + Assert.assertEquals(aViewPatientPage.getClinicalDiagnosisNames(), clinicalDiagnosisCheck); + Assert.assertEquals(aViewPatientPage.getFinalDiagnosisNames(), finalDiagnosisCheck); + Assert.assertEquals(aViewPatientPage.getAdditionalComments(), additionalCommentCheck); + Assert.assertEquals(sortedPubMedIDsFound, pubMedIDsCheck); + Assert.assertEquals(aViewPatientPage.getResolutionNotes(), resolutionNoteCheck); + + aHomePage.logOut(); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/LoginPageTest.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/LoginPageTest.java new file mode 100644 index 00000000..603cd3c1 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/LoginPageTest.java @@ -0,0 +1,68 @@ +package org.phenotips.endtoendtests.testcases; + +import org.phenotips.endtoendtests.pageobjects.HomePage; +import org.phenotips.endtoendtests.pageobjects.LoginPage; + +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * Test the login functionality. + * + * Each test can be run separately. + */ +public class LoginPageTest extends BaseTest +{ + HomePage currentPage = new HomePage(theDriver); + + LoginPage aLoginPage = new LoginPage(theDriver); + + /** + * Login as Admin, assert that both Administrator Settings link and About link are visible + */ + @Test + public void loginAdminTest() + { + currentPage.navigateToLoginPage() + .loginAsAdmin(); + + // This is a basic test so we touch a selector directly. + // But for all other tests, don't touch selectors directly - abstract the action out to a method in a pageObject. + // Ex. isAdminLinkVisible() + Assert.assertTrue(currentPage.isElementPresent(currentPage.adminLink)); + Assert.assertTrue(currentPage.isElementPresent(currentPage.aboutLink)); + + currentPage.logOut(); + } + + /** + * Login as User, assert that Admin Settings link is not visible + */ + @Test + public void loginUserTest() + { + currentPage.navigateToLoginPage() + .loginAsUser(); + + Assert.assertFalse(currentPage.isElementPresent(currentPage.adminLink)); + Assert.assertTrue(currentPage.isElementPresent(currentPage.aboutLink)); + + currentPage.logOut(); + } + + /** + * Test for invalid credentials. This implicitly tests for it as we should have stayed on the login page rather than + * being redirected to the homepage. If we got logged in at any point, test fails as it cannot login again without + * having logging out. + */ + @Test + public void invalidCredentials() + { + currentPage.navigateToLoginPage() + .loginAs("TestUser1Uno", "BadPassword"); // Valid username but invalid password. + aLoginPage.loginAs("", "123"); // Empty username + aLoginPage.loginAs("SomeoneLikeYou", ""); // Empty password + + aLoginPage.loginAsAdmin().logOut(); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/MatchNotificationPageTests.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/MatchNotificationPageTests.java new file mode 100644 index 00000000..e4a8dff3 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/MatchNotificationPageTests.java @@ -0,0 +1,248 @@ +package org.phenotips.endtoendtests.testcases; + +import org.phenotips.endtoendtests.common.CommonInfoEnums; +import org.phenotips.endtoendtests.pageobjects.AdminRefreshMatchesPage; +import org.phenotips.endtoendtests.pageobjects.EmailUIPage; +import org.phenotips.endtoendtests.pageobjects.HomePage; +import org.phenotips.endtoendtests.pageobjects.ViewPatientPage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import net.bytebuddy.utility.RandomString; + +/** + * This class will contain tests for the match notification table. It will ensure the appropriate patients are present + * and show up when matched. For instance, match to genes, phenotype, and both. Requires MockMock email UI to be setup + * with the PC instance that is being tested. + */ +public class MatchNotificationPageTests extends BaseTest implements CommonInfoEnums +{ + final private String randomChars = RandomString.make(5); + + HomePage aHomePage = new HomePage(theDriver); + + EmailUIPage aEmailUIPage = new EmailUIPage(theDriver); + + ViewPatientPage aViewPatientPage = new ViewPatientPage(theDriver); + + AdminRefreshMatchesPage anAdminRefreshMatchesPage = new AdminRefreshMatchesPage(theDriver); + + /** + * Refresh "matches since last modified" so that the number goes to zero before beginning these tests. This ensures + * that the assertions for "Number of Patients Processed" will pass as we check for exact match. It then navigates + * to MockMock's UI page to clear the email inbox before any of these tests run. This method runs once before the + * tests in this class begin running. + */ + @BeforeClass + public void refreshMatchesFirst() + { + aHomePage.navigateToLoginPage() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToRefreshMatchesPage() + .refreshMatchesSinceLastUpdate() + .navigateToEmailInboxPage() + .deleteAllEmails() + .navigateToHomePage() + .logOut(); + } + + /** + * Creates two new patients with 3/4 same phenotypes and no identical genes. Asserts that: - Two new patients are + * processed after a match refresh since last update - A match for the two patients is found. + */ + @Test() + public void matchPhenotypeOnly() + { + List loPhenotypesToAdd1 = new ArrayList(Arrays.asList( + "Unilateral deafness", "Poikilocytosis", "Swollen lip", "Narcolepsy", "Eye poking")); + + List loPhenotypesToAdd2 = new ArrayList(Arrays.asList( + "Nausea and vomiting", "Poikilocytosis", "Swollen lip", "Narcolepsy", "Eye poking")); + + final String patientUniqueIdentifier = "PhenoOnlyMatch " + randomChars; + + String createdPatient1; + String createdPatient2; + List patientsEmailTitleCheck; + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToCreateANewPatientPage() + .toggleFirstFourConsentBoxes() + .updateConsent() + .setIdentifer(patientUniqueIdentifier) + .setDOB("05", "2005") + .setGender("Female") + .expandSection(SECTIONS.ClinicalSymptomsSection) + .addPhenotypes(loPhenotypesToAdd1) + .expandSection(SECTIONS.ClinicalSymptomsSection) + .expandSection(SECTIONS.GenotypeInfoSection) + .addGene("IBD3", "Candidate", "Sequencing") + .addGene("RNU1-4", "Rejected candidate", "Sequencing") + .addGene("DAW1", "Confirmed causal", "Sequencing") + .addGene("QRICH2", "Carrier", "Sequencing") + .addGene("SCN5A", "Tested negative", "Sequencing") + .saveAndViewSummary(); + + createdPatient1 = aViewPatientPage.getPatientID(); + + aViewPatientPage.logOut() + .loginAsUserTwo() + .navigateToCreateANewPatientPage() + .toggleFirstFourConsentBoxes() + .updateConsent() + .setIdentifer(patientUniqueIdentifier + "Matchee") + .setDOB("09", "2008") + .setGender("Male") + .expandSection(SECTIONS.ClinicalSymptomsSection) + .addPhenotypes(loPhenotypesToAdd2) + .expandSection(SECTIONS.ClinicalSymptomsSection) + .expandSection(SECTIONS.GenotypeInfoSection) + .addGene("NUDT12", "Candidate", "Sequencing") + .addGene("MIR5685", "Rejected candidate", "Sequencing") + .addGene("QRFP", "Confirmed causal", "Sequencing") + .addGene("LINC01854", "Carrier", "Sequencing") + .addGene("PRKCZ", "Tested negative", "Sequencing") + .saveAndViewSummary(); + + createdPatient2 = aViewPatientPage.getPatientID(); + + aViewPatientPage.logOut() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToRefreshMatchesPage() + .refreshMatchesSinceLastUpdate(); + + Assert.assertEquals(anAdminRefreshMatchesPage.getNumberOfLocalPatientsProcessed(), "2"); + + anAdminRefreshMatchesPage.navigateToAdminSettingsPage() + .navigateToMatchingNotificationPage() + .setAverageScoreSliderToMinimum() + .setGenotypeSliderToZero() + .filterByID(createdPatient1) + .emailSpecificPatients(createdPatient1, createdPatient2) + .navigateToEmailInboxPage(); + + patientsEmailTitleCheck = new ArrayList<>(Arrays.asList( + "Matches found for patient: " + createdPatient2, "Matches found for patient: " + createdPatient1)); + + Assert.assertTrue(aEmailUIPage.getEmailTitles().containsAll(patientsEmailTitleCheck)); + + aEmailUIPage.navigateToHomePage().logOut(); + } + + /** + * Creates two new patients with identical genotype and no identical phenotype. Asserts that: - Two new patients are + * processed after a match refresh since last update - A match for the two patients is found. + */ + @Test() + public void matchGenotypeOnly() + { + List loPhenotypesToAdd1 = new ArrayList(Arrays.asList( + "Unilateral deafness", "Tarsal synostosis", "Impairment of activities of daily living", + "Obliteration of the pulp chamber", "Abnormal proportion of marginal zone B cells")); + + List loPhenotypesToAdd2 = new ArrayList(Arrays.asList( + "Nausea and vomiting", "Poikilocytosis", "Swollen lip", "Narcolepsy", "Eye poking")); + + final String patientUniqueIdentifier = "GenoOnlyMatch " + randomChars; + + String createdPatient1; + String createdPatient2; + List patientsEmailTitleCheck; + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToCreateANewPatientPage() + .toggleFirstFourConsentBoxes() + .updateConsent() + .setIdentifer(patientUniqueIdentifier) + .setDOB("08", "2008") + .setGender("Female") + .expandSection(SECTIONS.ClinicalSymptomsSection) + .addPhenotypes(loPhenotypesToAdd1) + .expandSection(SECTIONS.ClinicalSymptomsSection) + .expandSection(SECTIONS.GenotypeInfoSection) + .addGene("DMPK", "Candidate", "Sequencing") + .addGene("SLC25A34", "Candidate", "Sequencing") + .addGene("USP9YP26", "Rejected candidate", "Sequencing") + .addGene("SH2B3", "Confirmed causal", "Sequencing") + .addGene("RIOK1", "Confirmed causal", "Sequencing") + .addGene("RIOK2", "Carrier", "Sequencing") + .addGene("RIOK3", "Tested negative", "Sequencing") + .saveAndViewSummary(); + + createdPatient1 = aViewPatientPage.getPatientID(); + + aViewPatientPage.logOut() + .loginAsUserTwo() + .navigateToCreateANewPatientPage() + .toggleFirstFourConsentBoxes() + .updateConsent() + .setIdentifer(patientUniqueIdentifier + "Matchee") + .setDOB("10", "2010") + .setGender("Male") + .expandSection(SECTIONS.ClinicalSymptomsSection) + .addPhenotypes(loPhenotypesToAdd2) + .expandSection(SECTIONS.ClinicalSymptomsSection) + .expandSection(SECTIONS.GenotypeInfoSection) + .addGene("DMPK", "Candidate", "Sequencing") + .addGene("SLC25A34", "Candidate", "Sequencing") + .addGene("USP9YP26", "Rejected candidate", "Sequencing") + .addGene("SH2B3", "Confirmed causal", "Sequencing") + .addGene("RIOK1", "Confirmed causal", "Sequencing") + .addGene("RIOK2", "Carrier", "Sequencing") + .addGene("RIOK3", "Tested negative", "Sequencing") + .saveAndViewSummary(); + + createdPatient2 = aViewPatientPage.getPatientID(); + + aViewPatientPage.logOut() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToRefreshMatchesPage() + .refreshMatchesSinceLastUpdate(); + + Assert.assertEquals(anAdminRefreshMatchesPage.getNumberOfLocalPatientsProcessed(), "2"); + + anAdminRefreshMatchesPage.navigateToAdminSettingsPage() + .navigateToMatchingNotificationPage() + .filterByID(createdPatient1) + .emailSpecificPatients(createdPatient1, createdPatient2) + .navigateToEmailInboxPage(); + + patientsEmailTitleCheck = new ArrayList<>(Arrays.asList( + "Matches found for patient: " + createdPatient2, "Matches found for patient: " + createdPatient1)); + + Assert.assertTrue(aEmailUIPage.getEmailTitles().containsAll(patientsEmailTitleCheck)); + + aEmailUIPage.navigateToHomePage().logOut(); + } + + @Test(enabled = false) + public void filterAndEmailTemp() + { + List patientsEmailTitleCheck = new ArrayList<>(Arrays.asList( + "Matches found for patient: P0000002", "Matches found for patient: P0000001")); + + aHomePage.navigateToLoginPage() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToMatchingNotificationPage() + .toggleContactedStatusCheckbox() + .emailSpecificPatients("P0000001", "P0000002 : Auto YnoP6 Patient") + .logOut() + .navigateToEmailInboxPage(); + + Assert.assertEquals(aEmailUIPage.getEmailTitles(), patientsEmailTitleCheck); + + aEmailUIPage.deleteAllEmails(); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/PatientCreationOptionsTests.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/PatientCreationOptionsTests.java new file mode 100644 index 00000000..ab296c47 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/PatientCreationOptionsTests.java @@ -0,0 +1,446 @@ +package org.phenotips.endtoendtests.testcases; + +import org.phenotips.endtoendtests.common.CommonInfoEnums; +import org.phenotips.endtoendtests.pageobjects.CreatePatientPage; +import org.phenotips.endtoendtests.pageobjects.HomePage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * This class of tests will eventually cycle through the possible options when creating a patient via manual input. If a + * change causes a section or some options to disappear, it should fail due to missing selectors. + */ +public class PatientCreationOptionsTests extends BaseTest implements CommonInfoEnums +{ + private HomePage aHomePage = new HomePage(theDriver); + + private CreatePatientPage aCreationPage = new CreatePatientPage(theDriver); + + // Cycle through all the options on the "Patient Information" Section. + @Test() + public void cycleThroughInfoOptions() + { + final List checkOnsetLabels = new ArrayList(Arrays.asList( + "Unknown", "Congenital onset", "Antenatal onset", "Embryonal onset", "Fetal onset", "Neonatal onset", + "Infantile onset", "Childhood onset", "Juvenile onset", "Adult onset", "Young adult onset", + "Middle age onset", "Late onset")); + + final List checkInheritanceLabels = new ArrayList(Arrays.asList( + "Sporadic", "Autosomal dominant inheritance", "Sex-limited autosomal dominant", + "Male-limited autosomal dominant", "Autosomal dominant somatic cell mutation", + "Autosomal dominant contiguous gene syndrome", "Autosomal recessive inheritance", + "Sex-limited autosomal recessive inheritance", + "Gonosomal inheritance", "X-linked inheritance", "X-linked dominant inheritance", + "X-linked recessive inheritance", "Y-linked inheritance", "Multifactorial inheritance", + "Digenic inheritance", "Oligogenic inheritance", "Polygenic inheritance", + "Mitochondrial inheritance")); + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToCreateANewPatientPage() + .toggleFirstFourConsentBoxes() + .toggleNthConsentBox(5) + .toggleNthConsentBox(5) + .updateConsent() + .setIdentifer("Auto Cycling Options") + .setLifeStatus("Alive") + .setLifeStatus("Deceased"); + + for (int i = 1; i <= 12; ++i) { + if (i < 10) { + aCreationPage.setDOB("0" + i, "2019"); + aCreationPage.setDateOfDeath("0" + i, "2019"); + } else { + aCreationPage.setDOB(String.valueOf(i), "2019"); + aCreationPage.setDateOfDeath(String.valueOf(i), "2019"); + } + } + + aCreationPage.setLifeStatus("Alive") + .setGender("Male") + .setGender("Female") + .setGender("Other") + .setGender("Unknown") + .setGender("Male"); + + List loAgeOnsetLabels = aCreationPage.cycleThroughAgeOfOnset(); + List loModeOfInheritanceLabels = aCreationPage.cycleThroughModeOfInheritance(); + + Assert.assertEquals(loAgeOnsetLabels, checkOnsetLabels); + Assert.assertEquals(loModeOfInheritanceLabels, checkInheritanceLabels, + "Actual: " + loModeOfInheritanceLabels + "\n Expected: " + checkInheritanceLabels + "\n"); + + aCreationPage.cycleThroughModeOfInheritance(); + aCreationPage.setIndicationForReferral("Now cycle through the other sections...") + .expandSection(SECTIONS.FamilyHistorySection); + + aCreationPage.navigateToPedigreeEditor("") + .closeEditor("save") + .saveAndViewSummary() + .logOut(); + } + + @Test() + public void cycleThroughFamilialConditions() + { + final List checkFamilialConditionsLabels = new ArrayList(Arrays.asList( + "Other affected relatives", "Consanguinity", "Parents with at least 3 miscarriages")); + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .expandSection(SECTIONS.FamilyHistorySection) + .setEthnicity("Paternal", "Han Chinese") + .setEthnicity("Maternal", "European Americans") + .setHealthConditionsFoundInFamily("There are some conditions here: \n Bla bla bla"); + + List loFamilialConditions = aCreationPage.cycleThroughFamilialHealthConditions(); + Assert.assertEquals(loFamilialConditions, checkFamilialConditionsLabels); + + aCreationPage.logOut().dismissUnsavedChangesWarning(); + } + + @Test() + public void cycleThroughPrenatalHistory() + { + final List checkPrenatalConditionsLabels = new ArrayList(Arrays.asList( + "Multiple gestation", "Conception after fertility medication", "Intrauterine insemination (IUI)", + "In vitro fertilization", "Intra-cytoplasmic sperm injection", "Gestational surrogacy", + "Donor egg", "Donor sperm", "Hyperemesis gravidarum (excessive vomiting)", + "Maternal hypertension", "Maternal diabetes", "Maternal fever in pregnancy", + "Intrapartum fever", "Maternal first trimester fever", "Maternal seizures", + "Maternal teratogenic exposure", "Toxemia of pregnancy", "Eclampsia", + "Maternal hypertension", "Preeclampsia", "Abnormal maternal serum screening", + "High maternal serum alpha-fetoprotein", "High maternal serum chorionic gonadotropin", + "Low maternal serum PAPP-A", "Low maternal serum alpha-fetoprotein", + "Low maternal serum chorionic gonadotropin", + "Low maternal serum estriol", "Intrauterine growth retardation", "Mild intrauterine growth retardation", + "Moderate intrauterine growth retardation", "Severe intrauterine growth retardation", + "Oligohydramnios", "Polyhydramnios", "Decreased fetal movement", + "Fetal akinesia sequence", "Increased fetal movement", "Abnormal delivery (Non-NSVD)", + "Vaginal birth after Caesarian", "Induced vaginal delivery", "Breech presentation", + "Complete breech presentation", "Frank breech presentation", "Incomplete breech presentation", + "Caesarian section", "Primary Caesarian section", "Secondary Caesarian section", + "Forceps delivery", "Ventouse delivery", "Delivery by Odon device", + "Spontaneous abortion", "Recurrent spontaneous abortion", "Premature birth", + "Premature birth following premature rupture of fetal membranes", + "Premature delivery because of cervical insufficiency or membrane fragility", + "Small for gestational age (<-2SD)", "Large for gestational age (>+2SD)", "Small birth length (<-2SD)", + "Large birth length (>+2SD)", "Congenital microcephaly (<-3SD)", "Congenital macrocephaly (>+2SD)", + "Neonatal respiratory distress", "Neonatal asphyxia", "Neonatal inspiratory stridor", + "Prolonged neonatal jaundice", "Poor suck", "Neonatal hypoglycemia", "Neonatal sepsis")); + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .expandSection(SECTIONS.PrenatalHistorySection); + + List loPrenatalYesNoBoxes = aCreationPage.cycleThroughPrenatalHistory(); + Assert.assertEquals(loPrenatalYesNoBoxes, checkPrenatalConditionsLabels); + + aCreationPage.cycleThroughPrenatalOptions().logOut().dismissUnsavedChangesWarning(); + } + + @Test() + public void cycleThroughPhenotypeDetails() + { + final List checkPhenotypeDetailsLabels = new ArrayList(Arrays.asList( + "", "", "Age of onset:", "Unknown", "Congenital onset", "Antenatal onset", + "Embryonal onset", "Fetal onset", "Neonatal onset", "Infantile onset", "Childhood onset", + "Juvenile onset", "Adult onset", "Young adult onset", "Middle age onset", "Late onset", + "Pace of progression:", "Unknown", "Nonprogressive", "Slow progression", "Progressive", + "Rapidly progressive", "Variable progression rate", "Severity:", "Unknown", "Borderline", + "Mild", "Moderate", "Severe", "Profound", "Temporal pattern:", "Unknown", "Insidious onset", + "Chronic", "Subacute", "Acute", "Spatial pattern:", "Unknown", "Generalized", "Localized", "Distal", + "Proximal", "Laterality:", "Unknown", "Bilateral", "Unilateral", "Right", "Left", "Comments:", + "Image / photo (optional):", "+", "Medical report (optional):", "+")); + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .expandSection(SECTIONS.ClinicalSymptomsSection) + .addPhenotype("Small earlobe") + .addDetailsToNthPhenotype(1); + + List loPhenotypeDetailsOptions = aCreationPage.cycleThroughPhenotypeDetailsLabels(); + System.out.println(loPhenotypeDetailsOptions); + Assert.assertEquals(loPhenotypeDetailsOptions, checkPhenotypeDetailsLabels); + + aCreationPage.logOut().dismissUnsavedChangesWarning(); + } + + // Checks only first level depth on HPO tree. Structure is rather inconsistent, can't figure out + // a simple recursive function for it. + @Test() + public void cycleThroughAllPhenotypes() + { + final List checkPhenotypeLabels = new ArrayList<>(Arrays.asList( + "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "Decreased body mass index", "Eunuchoid habitus", "Failure to thrive", + "Slender build", "Small for gestational age", "Weight loss", "Large for gestational age", "Obesity", + "Overweight", "Asymmetric short stature", "Birth length less than 3rd percentile", + "Disproportionate short stature", "Pituitary dwarfism", "Proportionate short stature", + "Birth length greater than 97th percentile", "Disproportionate tall stature", "Overgrowth", + "Proportionate tall stature", "Slender build", "Congenital microcephaly", "Postnatal microcephaly", + "Progressive microcephaly", "Macrocephaly at birth", "Postnatal macrocephaly", "Progressive macrocephaly", + "Relative macrocephaly", "Failure to thrive in infancy", "Severe failure to thrive", + "Hemihypertrophy of lower limb", "Hemihypertrophy of upper limb", "Coronal craniosynostosis", + "Lambdoidal craniosynostosis", "Multiple suture craniosynostosis", "Orbital craniosynostosis", + "Sagittal craniosynostosis", "Incomplete cleft of the upper lip", "Median cleft lip", + "Non-midline cleft lip", "Submucous cleft lip", "Cleft primary palate", "Cleft secondary palate", + "Median cleft palate", "Non-midline cleft palate", "Submucous cleft of soft and hard palate", + "Abnormality of the shape of the midface", "Bird-like facies", "Coarse facial features", + "Craniofacial disproportion", "Doll-like facies", "Elfin facies", "Facial asymmetry", + "Facial shape deformation", "Flat face", "Large face", "Moon facies", "Oval face", "Round face", + "Small face", "Square face", "Triangular face", "Cerebral visual impairment", "Reduced visual acuity", + "Visual field defect", "Visual loss", "Abnormal corneal endothelium morphology", + "Abnormal corneal epithelium morphology", "Abnormality of corneal shape", "Abnormality of corneal size", + "Abnormality of corneal stroma", "Abnormality of corneal thickness", "Abnormality of the corneal limbus", + "Abnormality of the curvature of the cornea", "Abnormality of the line of Schwalbe", "Cornea verticillata", + "Corneal degeneration", "Corneal dystrophy", "Corneal neovascularization", "Corneal opacity", + "Corneal perforation", "Decreased corneal reflex", "Decreased corneal sensation", "Epibulbar dermoid", + "Chorioretinal coloboma", "Ciliary body coloboma", "Iris coloboma", "Lens coloboma", "Optic nerve coloboma", + "Retinal coloboma", "Abnormal trabecular meshwork morphology", "Absent anterior chamber of the eye", + "Anterior chamber cells", "Anterior chamber cyst", "Anterior chamber flare", + "Anterior chamber inflammatory cells", "Anterior chamber red blood cells", "Anterior chamber synechiae", + "Corneolenticular adhesion", "Deep anterior chamber", "Hypopyon", "Shallow anterior chamber", + "Age-related cataract", "Capsular cataract", "Christmas tree cataract", "Congenital cataract", + "Juvenile cataract", "Membranous cataract", "Polar cataract", "Presenile cataracts", "Progressive cataract", + "Subcapsular cataract", "Zonular cataract", "Abnormal chorioretinal morphology", + "Abnormal macular morphology", "Abnormality of retinal pigmentation", + "Abnormality of the retinal vasculature", "Angioid streaks of the fundus", + "Aplasia/Hypoplasia of the retina", "Hypermyelinated retinal nerve fibers", "Intraretinal fluid", + "Retinal coloboma", "Retinal degeneration", "Retinal detachment", "Retinal dysplasia", "Retinal dystrophy", + "Retinal fold", "Retinal hamartoma", "Retinal hemorrhage", "Retinal infarction", "Retinal neoplasm", + "Retinal perforation", "Retinal thinning", "Retinopathy", "Retinoschisis", "Sub-RPE deposits", + "Subretinal deposits", "Subretinal fluid", "Yellow/white lesions of the retina", + "Abnormality of optic chiasm morphology", "Abnormality of the optic disc", + "Aplasia/Hypoplasia of the optic nerve", "Leber optic atrophy", "Marcus Gunn pupil", + "Morning glory anomaly", "Optic nerve arteriovenous malformation", "Optic nerve coloboma", + "Optic nerve compression", "Optic nerve dysplasia", "Optic nerve misrouting", "Optic neuritis", + "Optic neuropathy", "Bilateral microphthalmos", "Unilateral microphthalmos", "Congenital nystagmus", + "Divergence nystagmus", "Gaze-evoked nystagmus", "Horizontal nystagmus", "Nystagmus-induced head nodding", + "Pendular nystagmus", "Rotary nystagmus", "Vertical nystagmus", "Vestibular nystagmus", + "Concomitant strabismus", "Cyclodeviation", "Esodeviation", "Exodeviation", "Heterophoria", "Heterotropia", + "Hyperdeviation", "Hypodeviation", "Incomitant strabismus", "Monocular strabismus", "Neurogenic strabismus", + "Adult onset sensorineural hearing impairment", "Bilateral sensorineural hearing impairment", + "Childhood onset sensorineural hearing impairment", "Congenital sensorineural hearing impairment", + "High-frequency sensorineural hearing impairment", "Low-frequency sensorineural hearing impairment", + "Mild neurosensory hearing impairment", "Mixed hearing impairment", + "Moderate sensorineural hearing impairment", "Old-aged sensorineural hearing impairment", + "Profound sensorineural hearing impairment", "Progressive sensorineural hearing impairment", + "Severe sensorineural hearing impairment", "Bilateral conductive hearing impairment", + "Congenital conductive hearing impairment", "Mild conductive hearing impairment", + "Mixed hearing impairment", "Moderate conductive hearing impairment", + "Progressive conductive hearing impairment", "Severe conductive hearing impairment", + "Unilateral conductive hearing impairment", "Abnormal location of ears", + "Abnormality of cartilage of external ear", "Abnormality of the auditory canal", "Abnormality of the pinna", + "Abnormality of the tympanic membrane", "Aplasia/Hypoplasia of the external ear", + "Bilateral external ear deformity", "External ear malformation", "Extra concha fold", + "Hypertrophic auricular cartilage", "Neoplasm of the outer ear", "Polyotia", "Telangiectasia of the ear", + "Unilateral external ear deformity", "Abnormality of the round window", + "Abnormality of the vestibular window", "Functional abnormality of the inner ear", + "Morphological abnormality of the inner ear", "Neoplasm of the inner ear", "Generalized hyperpigmentation", + "Hyperpigmentation in sun-exposed areas", "Irregular hyperpigmentation", "Melasma", + "Mixed hypo- and hyperpigmentation of the skin", "Progressive hyperpigmentation", + "Absent skin pigmentation", "Confetti hypopigmentation pattern of lower leg skin", + "Generalized hypopigmentation", "Hypomelanotic macule", "Hypopigmented skin patches", + "Hypopigmented streaks", "Mixed hypo- and hyperpigmentation of the skin", "Partial albinism", "Piebaldism", + "Facial capillary hemangioma", "Periocular capillary hemangioma", "Pulmonary capillary hemangiomatosis", + "Angioedema", "Angiokeratoma", "Cutis marmorata", "Erythema", "Non-pruritic urticaria", + "Prominent superficial blood vessels", "Subcutaneous hemorrhage", "Telangiectasia", "Urticaria", + "Vasculitis in the skin", "Patent foramen ovale", "Primum atrial septal defect", + "Secundum atrial septal defect", "Sinus venosus atrial septal defect", "Swiss cheese atrial septal defect", + "Unroofed coronary sinus", "Gerbode ventricular septal defect", "Inlet ventricular septal defect", + "Muscular ventricular septal defect", "Non-restrictive ventricular septal defect", + "Perimembranous ventricular septal defect", "Restrictive ventricular septal defect", + "Subarterial ventricular septal defect", "Coarctation in the transverse aortic arch", + "Coarctation of abdominal aorta", "Coarctation of the descending aortic arch", + "Long segment coarctation of the aorta", "Tetralogy of Fallot with absent pulmonary valve", + "Tetralogy of Fallot with absent subarterial conus", + "Tetralogy of Fallot with atrioventricular canal defect", "Tetralogy of Fallot with pulmonary atresia", + "Tetralogy of Fallot with pulmonary stenosis", "Atrial cardiomyopathy", "Dilated cardiomyopathy", + "Histiocytoid cardiomyopathy", "Hypertrophic cardiomyopathy", "Noncompaction cardiomyopathy", + "Restrictive cardiomyopathy", "Right ventricular cardiomyopathy", "Takotsubo cardiomyopathy", + "Abnormal electrophysiology of sinoatrial node origin", "Abnormal heart rate variability", "Bradycardia", + "Cardiac arrest", "Palpitations", "Supraventricular arrhythmia", "Tachycardia", "Ventricular arrhythmia", + "Central diaphragmatic hernia", "Morgagni diaphragmatic hernia", "Posterolateral diaphragmatic hernia", + "Abnormal chest radiograph finding (lung)", "Abnormal lung lobation", "Abnormal pulmonary lymphatics", + "Abnormal subpleural morphology", "Abnormality of the pleura", "Abnormality of the pulmonary vasculature", + "Alveolar proteinosis", "Aplasia/Hypoplasia of the lungs", "Atelectasis", "Bronchogenic cyst", + "Bronchopulmonary sequestration", "Chronic lung disease", "Cystic lung disease", "Emphysema", + "Hypersensitivity pneumonitis", "Interstitial pulmonary abnormality", + "Intraalveolar nodular calcifications", "Lung abscess", "Neoplasm of the lung", "Pneumothorax", + "Pulmonary edema", "Pulmonary eosinophilic infiltration", "Pulmonary fibrosis", "Pulmonary granulomatosis", + "Pulmonary hemorrhage", "Pulmonary infiltrates", "Pulmonary opacity", "Pulmonary pneumatocele", + "Respiratory tract infection", "Unilateral primary pulmonary dysgenesis", "Diaphyseal dysplasia", + "Epiphyseal dysplasia", "Lethal skeletal dysplasia", "Metaphyseal dysplasia", + "Multiple epiphyseal dysplasia", "Multiple skeletal anomalies", "Spondyloepimetaphyseal dysplasia", + "Spondyloepiphyseal dysplasia", "Spondylometaphyseal dysplasia", + "Bowing of limbs due to multiple fractures", "Multiple prenatal fractures", + "Painless fractures due to injury", "Pathologic fracture", "Recurrent fractures", "Short lower limbs", + "Forearm undergrowth", "Bilateral camptodactyly", "Camptodactyly of 2nd-5th fingers", + "Congenital finger flexion contractures", + "Contracture of the proximal interphalangeal joint of the 2nd finger", + "Contracture of the proximal interphalangeal joint of the 3rd finger", + "Contracture of the proximal interphalangeal joint of the 4th finger", + "Contracture of the proximal interphalangeal joint of the 5th finger", + "Contracture of the proximal interphalangeal joint of the 2nd toe", + "Contracture of the proximal interphalangeal joint of the 3rd toe", + "Contracture of the proximal interphalangeal joint of the 4th toe", + "Contractures of the proximal interphalangeal joint of the 5th toe", "1-2 finger syndactyly", + "1-3 finger syndactyly", "1-4 finger syndactyly", "1-5 finger syndactyly", "2-3 finger syndactyly", + "2-4 finger syndactyly", "2-5 finger syndactyly", "3-4 finger syndactyly", "3-5 finger syndactyly", + "4-5 finger syndactyly", "Cutaneous finger syndactyly", "Osseous finger syndactyly", "1-2 toe syndactyly", + "1-3 toe syndactyly", "1-4 toe syndactyly", "1-5 toe syndactyly", "2-3 toe syndactyly", + "2-4 toe syndactyly", "2-5 toe syndactyly", "3-4 toe syndactyly", "3-5 toe syndactyly", + "4-5 toe syndactyly", "Cutaneous syndactyly of toes", "Osseous syndactyly of toes", + "Preaxial foot polydactyly", "Preaxial hand polydactyly", "Postaxial foot polydactyly", + "Postaxial hand polydactyly", "Hand monodactyly", "Postaxial oligodactyly", "Unilateral oligodactyly", + "Foot monodactyly", "Compensatory scoliosis", "Kyphoscoliosis", "Progressive congenital scoliosis", + "Thoracic scoliosis", "Thoracolumbar scoliosis", "Abnormal sacrum morphology", + "Abnormal vertebral morphology", "Abnormality of the cervical spine", "Abnormality of the coccyx", + "Abnormality of the curvature of the vertebral column", "Abnormality of the intervertebral disk", + "Abnormality of the lumbar spine", "Abnormality of the odontoid process", + "Abnormality of the thoracic spine", "Aplasia/Hypoplasia involving the vertebral column", + "Atlantoaxial abnormality", "Back pain", "Reversed usual vertebral column curves", "Spinal canal stenosis", + "Spinal deformities", "Spinal dysplasia", "Spinal instability", "Spinal rigidity", "Spondylolisthesis", + "Spondylolysis", "Congenital contracture", "Contractures of the large joints", + "Decreased cervical spine flexion due to contractures of posterior cervical muscles", + "Flexion contracture of digit", "Joint contractures involving the joints of the feet", + "Limb joint contracture", "Multiple joint contractures", "Progressive flexion contractures", + "Restricted neck movement due to contractures", "Bilateral talipes equinovarus", + "Talipes cavus equinovarus", "Proximal esophageal atresia", "Long-segment aganglionic megacolon", + "Short-segment aganglionic megacolon", "Total colonic aganglionosis", "Acholic stools", + "Cholestatic liver disease", "Extrahepatic cholestasis", "Intrahepatic cholestasis", "Jaundice", + "Abnormal liver function tests during pregnancy", "Elevated serum alanine aminotransferase", + "Elevated serum aspartate aminotransferase", "Elevated serum transaminases during infections", + "Diabetic ketoacidosis", "Insulin-resistant diabetes mellitus", "Maternal diabetes", + "Maturity-onset diabetes of the young", "Type I diabetes mellitus", "Type II diabetes mellitus", + "Cystic renal dysplasia", "Multicystic kidney dysplasia", "Multiple renal cysts", + "Multiple small medullary renal cysts", "Polycystic kidney dysplasia", "Renal cortical cysts", + "Renal corticomedullary cysts", "Renal diverticulum", "Solitary renal cyst", "Congenital megaureter", + "Hydroureter", "Neoplasm of the ureter", "Ureteral agenesis", "Ureteral atresia", "Ureteral duplication", + "Ureteral dysgenesis", "Ureteral obstruction", "Ureterocele", "Vesicoureteral reflux", + "Congenital megalourethra", "Displacement of the external urethral meatus", "Distal urethral duplication", + "Neoplasm of the urethra", "Patulous urethra", "Urethral atresia", "Urethral diverticulum", + "Urethral fistula", "Urethral obstruction", "Urethritis", "Urethrocele", "Urogenital sinus anomaly", + "Ambiguous genitalia", "female", "Ambiguous genitalia", "male", + "Gonadal tissue inappropriate for external genitalia or chromosomal sex", "Ovotestis", + "True hermaphroditism", "Coronal hypospadias", "Glandular hypospadias", "Midshaft hypospadias", + "Penile hypospadias", "Penoscrotal hypospadias", "Perineal hypospadias", "Scrotal hypospadias", + "Bilateral cryptorchidism", "Unilateral cryptorchidism", "Mild global developmental delay", + "Moderate global developmental delay", "Profound global developmental delay", + "Severe global developmental delay", "Delayed ability to sit", "Delayed ability to stand", + "Delayed ability to walk", "Absent speech", "Expressive language delay", "Receptive language delay", + "Dyscalculia", "Dyslexia", "Impaired visuospatial constructive cognition", "Abnormal consumption behavior", + "Abnormal emotion/affect behavior", "Abnormal social behavior", "Abnormal temper tantrums", + "Addictive behavior", "Autistic behavior", "Delusions", "Diminished ability to concentrate", "Drooling", + "Echolalia", "Hallucinations", "Hyperorality", "Impairment in personality functioning", + "Inflexible adherence to routines or rituals", "Lack of insight", "Lack of spontaneous play", + "Low frustration tolerance", "Mania", "Mutism", "Obsessive-compulsive behavior", + "Oppositional defiant disorder", "Perseveration", "Personality changes", "Photophobia", + "Pseudobulbar behavioral symptoms", "Psychomotor retardation", "Psychosis", "Restlessness", "Schizophrenia", + "Self-neglect", "Short attention span", "Sleep disturbance", "Sound sensitivity", + "Episodic generalized hypotonia", "Generalized hypotonia due to defect at the neuromuscular junction", + "Epileptic spasms", "Febrile seizures", "Focal-onset seizure", "Generalized-onset seizure", + "Multifocal seizures", "Nocturnal seizures", "Status epilepticus", "Symptomatic seizures", + "Cerebellar ataxia associated with quadrupedal gait", "Dysdiadochokinesis", "Dysmetria", "Dyssynergia", + "Episodic ataxia", "Gait ataxia", "Limb ataxia", "Nonprogressive cerebellar ataxia", + "Progressive cerebellar ataxia", "Spastic ataxia", "Truncal ataxia", "Axial dystonia", "Focal dystonia", + "Generalized dystonia", "Hemidystonia", "Limb dystonia", "Oculogyric crisis", "Paroxysmal dystonia", + "Torsion dystonia", "Choreoathetosis", "Clasp-knife sign", "Lower limb spasticity", "Opisthotonus", + "Progressive spasticity", "Spastic diplegia", "Spastic dysarthria", "Spastic gait", "Spastic hemiparesis", + "Spastic tetraparesis", "Spastic tetraplegia", "Spasticity of facial muscles", + "Spasticity of pharyngeal muscles", "Upper limb spasticity", "Spina bifida", "Abnormal CNS myelination", + "Abnormal meningeal morphology", "Abnormal neural tube morphology", "Abnormality of brain morphology", + "Abnormality of neuronal migration", "Abnormality of the cerebrospinal fluid", + "Abnormality of the glial cells", "Abnormality of the spinal cord", "Abnormality of the subarachnoid space", + "Alzheimer disease", "Aplasia/Hypoplasia involving the central nervous system", + "Atrophy/Degeneration affecting the central nervous system", "CNS infection", "Central nervous system cyst", + "Encephalocele", "Morphological abnormality of the pyramidal tract", + "Neoplasm of the central nervous system")); + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .expandSection(SECTIONS.ClinicalSymptomsSection); + + List loAllPhenotypes = aCreationPage.cycleThroughAllPhenotypes(); + System.out.println(loAllPhenotypes); + Assert.assertEquals(loAllPhenotypes, checkPhenotypeLabels); + + aCreationPage.logOut().dismissUnsavedChangesWarning(); + } + + // Clicks on all input boxes within the Diagnosis section and tries to provide input. + // Asserts that the PubMedIDs and Resolution Notes are hidden upon toggling "Case Solved" + @Test() + public void cycleThroughDiagnosis() + { + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .expandSection(SECTIONS.DiagnosisSection); + + System.out.println("Case Solved should be False: " + aCreationPage.isCaseSolved()); + Assert.assertFalse(aCreationPage.isCaseSolved()); + Assert.assertFalse(aCreationPage.isPubMedAndResolutionBoxesClickable()); + + aCreationPage.cycleThroughDiagnosisBoxes(); + System.out.println("Case Solved should be True: " + aCreationPage.isCaseSolved()); + Assert.assertTrue(aCreationPage.isCaseSolved()); + Assert.assertTrue(aCreationPage.isPubMedAndResolutionBoxesClickable()); + + aCreationPage.toggleCaseSolved(); + System.out.println("Case Solved should be False: " + aCreationPage.isCaseSolved()); + Assert.assertFalse(aCreationPage.isCaseSolved()); + Assert.assertFalse(aCreationPage.isPubMedAndResolutionBoxesClickable()); + + aCreationPage.logOut().dismissUnsavedChangesWarning(); + } + + // Checks that the red error message when inputting an invalid PubMed ID shows up. + @Test() + public void checkDiagnosisErrorMessages() + { + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .expandSection(SECTIONS.DiagnosisSection); + + aCreationPage.toggleCaseSolved() + .addPubMedID("This is an invalid ID"); + + Assert.assertFalse(aCreationPage.isNthPubMDBoxValid(1)); + + aCreationPage.removeNthPubMedID(1) + .addPubMedID("30699054"); + + Assert.assertTrue(aCreationPage.isNthPubMDBoxValid(1)); + + aCreationPage.logOut().dismissUnsavedChangesWarning(); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/PedigreePageTest.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/PedigreePageTest.java new file mode 100644 index 00000000..9e03484b --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/PedigreePageTest.java @@ -0,0 +1,265 @@ +package org.phenotips.endtoendtests.testcases; + +import org.phenotips.endtoendtests.common.CommonInfoEnums; +import org.phenotips.endtoendtests.pageobjects.CreatePatientPage; +import org.phenotips.endtoendtests.pageobjects.HomePage; +import org.phenotips.endtoendtests.pageobjects.PedigreeEditorPage; +import org.phenotips.endtoendtests.pageobjects.ViewPatientPage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import net.bytebuddy.utility.RandomString; + +/** + * Tests for the Pedigree Editor page and the sync with the patient info page. There are cases for creation of a patient + * and input information both via Pedigree Editor This should be run as an entire class due to the pedigree editor + * requiring certain kinds of patients to be present for the selectors to work. + */ +public class PedigreePageTest extends BaseTest implements CommonInfoEnums +{ + final private HomePage aHomePage = new HomePage(theDriver); + + final private PedigreeEditorPage aPedigreeEditorPage = new PedigreeEditorPage(theDriver); + + final private CreatePatientPage aCreatePatientPage = new CreatePatientPage(theDriver); + + final private ViewPatientPage aViewPatientPage = new ViewPatientPage(theDriver); + + final private String randomChars = RandomString.make(5); + + /** + * Creates a patient with phenotypes and genes. Asserts that they are reflected in the pedigree editor after a save. + * This tests the pedigree editor when one patient/node is present. Checks that Patient Form Info -> Pedigree Editor + * Info + */ + @Test() + public void basicPedigree() + { + final List checkPhenotypes = new ArrayList(Arrays.asList( + "Prominent nose", "Macrocephaly at birth", "Narcolepsy", "Large elbow", "Terminal insomnia", "Small hand")); + final List checkCandidateGenes = new ArrayList(Arrays.asList("IV", "IVD")); + final List checkConfirmedCausalGenes = new ArrayList(Arrays.asList("IVL")); + final List checkCarrierGenes = new ArrayList(Arrays.asList("OR6B1")); + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToCreateANewPatientPage() + .toggleFirstFourConsentBoxes() + .updateConsent() + .setIdentifer("Pedigree Editor " + randomChars) + .setDOB("05", "2000") + .setGender("Female") + .setOnset("Congenital onset ") + .expandSection(SECTIONS.ClinicalSymptomsSection) + .addPhenotypes(checkPhenotypes) + .expandSection(SECTIONS.ClinicalSymptomsSection) + .expandSection(SECTIONS.GenotypeInfoSection) + .addGene("IV", "Candidate", "Sequencing") + .addGene("IVD", "Candidate", "Sequencing") + .addGene("IVL", "Confirmed causal", "Sequencing") + .addGene("OR6B1", "Carrier", "Sequencing") + .expandSection(SECTIONS.FamilyHistorySection) + .navigateToPedigreeEditor("") + .openNthEditModal(1); + + List loPhenotypesFound = aPedigreeEditorPage.getPhenotypes(); + List loCandidateGenesFound = aPedigreeEditorPage.getGenes("Candidate"); + List loConfirmedCausalGenesFound = aPedigreeEditorPage.getGenes("Confirmed Causal"); + List loCarrierGenesFound = aPedigreeEditorPage.getGenes("Carrier"); + String patientGender = aPedigreeEditorPage.getGender(); + + Assert.assertEquals(loPhenotypesFound, checkPhenotypes); + Assert.assertEquals(loCandidateGenesFound, checkCandidateGenes); + Assert.assertEquals(loConfirmedCausalGenesFound, checkConfirmedCausalGenes); + Assert.assertEquals(loCarrierGenesFound, checkCarrierGenes); + Assert.assertEquals(patientGender, "Female"); + + aPedigreeEditorPage.closeEditor("Save") + .saveAndViewSummary() + .logOut(); + } + + /** + * Creates a child for the most recently created patient via the Pedigree editor. Asserts that two new patient nodes + * are created and the total number of nodes. + */ + @Test() + public void createChild() + { + aHomePage.navigateToLoginPage() + .loginAsAdmin() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .expandSection(SECTIONS.FamilyHistorySection) + .navigateToPedigreeEditor(""); + aPedigreeEditorPage.createChild("male"); + + Assert.assertEquals(aPedigreeEditorPage.getNumberOfTotalPatientsInTree(), 3); + Assert.assertEquals(aPedigreeEditorPage.getNumberOfPartnerLinks(), 1); + + aPedigreeEditorPage.createSibling(3); + + Assert.assertEquals(aPedigreeEditorPage.getNumberOfTotalPatientsInTree(), 4); + Assert.assertEquals(aPedigreeEditorPage.getNumberOfPartnerLinks(), 1); + + aPedigreeEditorPage.closeEditor("Don't Save") + .saveAndViewSummary() + .logOut(); + } + + /** + * Ensure changes made through pedigree editor are reflected on the View Patient Form after a save. Asserts: - + * Correct number of nodes (note, does not check the type/gender, just counts the hoverboxes) - Phenotypes + * correspond on the view Patient Info Form + */ + @Test() + public void editorToPatientForm() + { + List loPhenotypesToAdd = new ArrayList<>(Arrays.asList("Small hand", "Large knee", "Acne")); + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToCreateANewPatientPage() + .toggleFirstFourConsentBoxes() + .updateConsent() + .setGender("Female") + .expandSection(SECTIONS.FamilyHistorySection) + .navigateToPedigreeEditor(""); + + Assert.assertEquals(aPedigreeEditorPage.getNumberOfTotalPatientsInTree(), 1); + Assert.assertEquals(aPedigreeEditorPage.getNumberOfPartnerLinks(), 0); + + aPedigreeEditorPage.openNthEditModal(1) + .addPhenotypes(loPhenotypesToAdd) + .closeEditor("Save") + .expandSection(SECTIONS.ClinicalSymptomsSection); + + List foundPhenotypesOnPatientForm = aCreatePatientPage.getAllPhenotypes(); + + System.out.println("Before: " + foundPhenotypesOnPatientForm); + System.out.println("Before loAdding: " + loPhenotypesToAdd); + // Must sort alphabetical first before comparison, they will be of a different order. + loPhenotypesToAdd.sort(String::compareTo); + foundPhenotypesOnPatientForm.sort(String::compareTo); + + Assert.assertEquals(foundPhenotypesOnPatientForm, loPhenotypesToAdd); + System.out.println("After: " + foundPhenotypesOnPatientForm); + System.out.println("After loAdding: " + loPhenotypesToAdd); + + aCreatePatientPage + .saveAndViewSummary() + .logOut(); + } + + /** + * Creates a new patient via the pedigree editor, using an existing pedigree created in the previous test Asserts: - + * Correct number of Patients and Partner Links before and after creating a male sibling. - Asserts that the + * phenotypes and genotype information on the modal corresponds to info on the patient's main profile page. This + * includes gene names, their status, strategies and comments. + */ + @Test() + public void createNewPatientViaEditor() + { + List loPhenotypesToAdd = new ArrayList<>(Arrays.asList("Small hand", "Large knee", "Acne")); + + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .expandSection(SECTIONS.FamilyHistorySection) + .navigateToPedigreeEditor(""); + + Assert.assertEquals(aPedigreeEditorPage.getNumberOfTotalPatientsInTree(), 1); + Assert.assertEquals(aPedigreeEditorPage.getNumberOfPartnerLinks(), 0); + + aPedigreeEditorPage.createSibling(1); + + Assert.assertEquals(aPedigreeEditorPage.getNumberOfTotalPatientsInTree(), 4); + Assert.assertEquals(aPedigreeEditorPage.getNumberOfPartnerLinks(), 1); + + aPedigreeEditorPage.openNthEditModal(5) + .linkPatient("New"); + + String createdPatient = aPedigreeEditorPage.getPatientIDFromModal(); + + aPedigreeEditorPage.addPhenotypes(loPhenotypesToAdd) + .addGene("LIN7C", "Candidate") + .addGene("TAOK3", "Confirmed Causal") + .addGene("YKT6", "Carrier") + .closeEditor("Save") + .saveAndViewSummary() + .navigateToAllPatientsPage() + .filterByPatientID(createdPatient) + .viewFirstPatientInTable() + .editThisPatient() + .toggleFirstFourConsentBoxes() + .updateConsent() + .expandSection(SECTIONS.ClinicalSymptomsSection); + + List foundPhenotypesFromPatientPage = aCreatePatientPage.getAllPhenotypes(); + + System.out.println("Before: " + foundPhenotypesFromPatientPage); + System.out.println("Before loAdding: " + loPhenotypesToAdd); + // Must sort alphabetical first before comparison, they will be of a different order. + loPhenotypesToAdd.sort(String::compareTo); + foundPhenotypesFromPatientPage.sort(String::compareTo); + + Assert.assertEquals(foundPhenotypesFromPatientPage, loPhenotypesToAdd); + System.out.println("After: " + foundPhenotypesFromPatientPage); + System.out.println("After loAdding: " + loPhenotypesToAdd); + + aCreatePatientPage.saveAndViewSummary(); + + List foundGeneNamesOnPatientForm = aViewPatientPage.getGeneNames(); + List checkGeneNames = new ArrayList<>(Arrays.asList("LIN7C", "TAOK3", "YKT6")); + + List foundGeneStatusesOnPatientForm = aViewPatientPage.getGeneStatus(); + List checkGeneStatuses = new ArrayList<>(Arrays.asList("Candidate", "Confirmed causal", "Carrier")); + + List foundGeneStrategiesOnPatientForm = aViewPatientPage.getGeneStrategies(); + List checkGeneStrategies = new ArrayList<>(Arrays.asList("", "", "")); + + List foundGeneCommentsOnPatientForm = aViewPatientPage.getGeneComments(); + List checkGeneComments = new ArrayList<>(Arrays.asList("", "", "")); + + Assert.assertEquals(foundGeneNamesOnPatientForm, checkGeneNames); + Assert.assertEquals(foundGeneStatusesOnPatientForm, checkGeneStatuses); + Assert.assertEquals(foundGeneStrategiesOnPatientForm, checkGeneStrategies); + Assert.assertEquals(foundGeneCommentsOnPatientForm, checkGeneComments); + + aViewPatientPage.logOut(); + } + + /** + * Opens the pedigree editor for the previously created patient (in the test above) and edit the patient's genotype. + * Asserts: - On trying to click "Close", that there is a js warning dialogue to prompt saving before navigating + * away. + */ + @Test() + public void warningDialoguePresent() + { + aHomePage.navigateToLoginPage() + .loginAsUser() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .editThisPatient() + .expandSection(SECTIONS.FamilyHistorySection) + .navigateToPedigreeEditor("") + .openNthEditModal(1) + .addGene("FOXP2", "Carrier"); + + Assert.assertTrue(aPedigreeEditorPage.doesWarningDialogueAppear()); + + aCreatePatientPage.saveAndViewSummary().logOut(); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/PermissionsTests.java b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/PermissionsTests.java new file mode 100644 index 00000000..611511f0 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/PermissionsTests.java @@ -0,0 +1,130 @@ +package org.phenotips.endtoendtests.testcases; + +import org.phenotips.endtoendtests.common.CommonInfoEnums; +import org.phenotips.endtoendtests.pageobjects.AdminMatchNotificationPage; +import org.phenotips.endtoendtests.pageobjects.AdminRefreshMatchesPage; +import org.phenotips.endtoendtests.pageobjects.HomePage; +import org.phenotips.endtoendtests.pageobjects.ViewPatientPage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import net.bytebuddy.utility.RandomString; + +/** + * This class tests that when permissions are modified, patients matching behaviour is modified. These tests must be run + * as a class. + */ +public class PermissionsTests extends BaseTest +{ + final private String randomChars = RandomString.make(5); + + HomePage aHomePage = new HomePage(theDriver); + + ViewPatientPage aViewPatientPage = new ViewPatientPage(theDriver); + + AdminRefreshMatchesPage anAdminRefreshMatchesPage = new AdminRefreshMatchesPage(theDriver); + + AdminMatchNotificationPage anAdminMatchNotificationPage = new AdminMatchNotificationPage(theDriver); + + /** + * Creates two patients with identical phenotypes and genotypes. One is "Private" and the other is "Matchable". + * Assert that: - 1 Patients Processed during refresh of matches - No match is found on Admin's match page + */ + @Test() + public void noMatchPrivatePatient() + { + List loPhenotypesToAdd = new ArrayList(Arrays.asList( + "Perimembranous ventricular septal defect", "Postaxial polysyndactyly of foot", + "Delayed ability to stand")); + + final String patientUniqueIdentifier = "NoPermissionForMatch " + randomChars; + + String createdPatient1; + String createdPatient2; + + aHomePage.navigateToLoginPage() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToRefreshMatchesPage() + .refreshMatchesSinceLastUpdate() // Refresh Matches so that "Since last update" goes to 0 first + .logOut() + + .loginAsUser() + .navigateToCreateANewPatientPage() + .toggleFirstFourConsentBoxes() + .updateConsent() + .setIdentifer(patientUniqueIdentifier) + .setDOB("01", "2001") + .setGender("Female") + .expandSection(CommonInfoEnums.SECTIONS.ClinicalSymptomsSection) + .addPhenotypes(loPhenotypesToAdd) + .expandSection(CommonInfoEnums.SECTIONS.ClinicalSymptomsSection) + .expandSection(CommonInfoEnums.SECTIONS.GenotypeInfoSection) + .addGene("CEP85", "Confirmed causal", "Sequencing") + .saveAndViewSummary(); + + aViewPatientPage.setGlobalVisibility("Private"); + + createdPatient1 = aViewPatientPage.getPatientID(); + + aViewPatientPage.logOut() + .loginAsUserTwo() + .navigateToCreateANewPatientPage() + .toggleFirstFourConsentBoxes() + .updateConsent() + .setIdentifer(patientUniqueIdentifier + "Matchee") + .setDOB("05", "2005") + .setGender("Male") + .expandSection(CommonInfoEnums.SECTIONS.ClinicalSymptomsSection) + .addPhenotypes(loPhenotypesToAdd) + .expandSection(CommonInfoEnums.SECTIONS.ClinicalSymptomsSection) + .expandSection(CommonInfoEnums.SECTIONS.GenotypeInfoSection) + .addGene("CEP85", "Confirmed causal", "Sequencing") + .saveAndViewSummary(); + + createdPatient2 = aViewPatientPage.getPatientID(); + + aViewPatientPage.logOut() + .loginAsAdmin() + .navigateToAdminSettingsPage() + .navigateToRefreshMatchesPage() + .refreshMatchesSinceLastUpdate(); + + Assert.assertEquals(anAdminRefreshMatchesPage.getNumberOfLocalPatientsProcessed(), "1"); + + anAdminRefreshMatchesPage.navigateToAdminSettingsPage() + .navigateToMatchingNotificationPage() + .filterByID(createdPatient1); + + Assert.assertFalse(anAdminMatchNotificationPage.doesMatchExist(createdPatient1, createdPatient2)); + + anAdminRefreshMatchesPage.logOut(); + } + + /** + * Ensure that the matchable patient created by User2Dos cannot be seen by User1Uno. Asserts that the unauthorized + * action error message page is presented. + */ + @Test() + public void cannotSeeOtherPatients() + { + String unauthorizedActionMsgCheck = "You are not allowed to view this page or perform this action."; + + aHomePage.navigateToLoginPage() + .loginAsUserTwo() + .navigateToAllPatientsPage() + .sortPatientsDateDesc() + .viewFirstPatientInTable() + .logOut() + .loginAsUser(); + + Assert.assertEquals(aHomePage.getUnauthorizedErrorMessage(), unauthorizedActionMsgCheck); + + anAdminRefreshMatchesPage.logOut(); + } +} diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/AllTests.xml b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/AllTests.xml new file mode 100644 index 00000000..3ab10008 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/AllTests.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/DemoTests.xml b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/DemoTests.xml new file mode 100644 index 00000000..1400d6cc --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/DemoTests.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/MultipleClasses.xml b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/MultipleClasses.xml new file mode 100644 index 00000000..385924dc --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/MultipleClasses.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/NoTests.xml b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/NoTests.xml new file mode 100644 index 00000000..9f480346 --- /dev/null +++ b/end-to-end-tests/src/test/java/org/phenotips/endtoendtests/testcases/xml/NoTests.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/end-to-end-tests/src/test/resources/allure.properties b/end-to-end-tests/src/test/resources/allure.properties new file mode 100644 index 00000000..80b02dde --- /dev/null +++ b/end-to-end-tests/src/test/resources/allure.properties @@ -0,0 +1 @@ +allure.results.directory=target/allure-results