// Copyright 2015 The Project Buendia Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at: http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distrib-
// uted under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
// OR CONDITIONS OF ANY KIND, either express or implied. See the License for
// specific language governing permissions and limitations under the License.
package org.projectbuendia.client.ui.chart;
import android.support.test.espresso.Espresso;
import android.support.test.espresso.IdlingResource;
import android.support.test.espresso.ViewInteraction;
import android.support.test.espresso.web.webdriver.Locator;
import android.test.suitebuilder.annotation.MediumTest;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.RadioButton;
import org.hamcrest.Matcher;
import org.odk.collect.android.views.MediaLayout;
import org.odk.collect.android.views.ODKView;
import org.odk.collect.android.widgets2.group.TableWidgetGroup;
import org.odk.collect.android.widgets2.selectone.ButtonsSelectOneWidget;
import org.projectbuendia.client.R;
import org.projectbuendia.client.events.FetchXformSucceededEvent;
import org.projectbuendia.client.events.SubmitXformSucceededEvent;
import org.projectbuendia.client.ui.FunctionalTestCase;
import org.projectbuendia.client.ui.sync.EventBusIdlingResource;
import org.projectbuendia.client.utils.Logger;
import org.projectbuendia.client.utils.Utils;
import java.util.UUID;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isJavascriptEnabled;
import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription;
import static android.support.test.espresso.web.assertion.WebViewAssertions.webMatches;
import static android.support.test.espresso.web.sugar.Web.onWebView;
import static android.support.test.espresso.web.webdriver.DriverAtoms.findElement;
import static android.support.test.espresso.web.webdriver.DriverAtoms.getText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
/** Functional tests for {@link PatientChartActivity}. */
@MediumTest
public class PatientChartActivityTest extends FunctionalTestCase {
private static final Logger LOG = Logger.create();
private static final String NO = "○";
private static final String YES = "●";
private static final int ROW_HEIGHT = 84;
private static final Matcher<View> OVERFLOW_BUTTON_MATCHER = anyOf(
allOf(isDisplayed(), withContentDescription("More options")),
allOf(isDisplayed(), withClassName(endsWith("OverflowMenuButton"))));;
public PatientChartActivityTest() {
super();
}
/**
* Tests that the general condition dialog successfully changes general condition.
* TODO/completeness: Currently disabled because this is now rendered in a WebView.
* A new test needs to be written that interacts with the WebView.
*/
/*
public void testGeneralConditionDialog_AppliesGeneralConditionChange() {
inUserLoginGoToDemoPatientChart();
click(viewWithId(R.id.patient_chart_vital_general_parent));
screenshot("General Condition Dialog");
click(viewWithText(R.string.status_well));
// Wait for a sync operation to update the chart.
EventBusIdlingResource<SyncFinishedEvent> syncFinishedIdlingResource =
new EventBusIdlingResource<>(UUID.randomUUID().toString(), mEventBus);
Espresso.registerIdlingResources(syncFinishedIdlingResource);
// Check for updated vital view.
expectVisibleSoon(viewWithText(R.string.status_well));
// Check for updated chart view.
expectVisible(viewThat(
hasText(R.string.status_short_desc_well),
not(hasId(R.id.patient_chart_vital_general_condition_number))));
}
*/
/** Tests that the encounter form can be opened more than once. */
public void testPatientChart_CanOpenEncounterFormMultipleTimes() {
inUserLoginGoToDemoPatientChart();
// Load the form once and dismiss it
openEncounterForm();
click(viewWithText("Discard"));
// Load the form again and dismiss it
openEncounterForm();
click(viewWithText("Discard"));
}
/**
* Tests that the admission date is correctly displayed in the header.
* TODO/completeness: Currently disabled. Re-enable once date picker
* selection works (supposedly works in Espresso 2.0).
*/
/*public void testPatientChart_ShowsCorrectAdmissionDate() {
mDemoPatient.admissionDate = Optional.of(DateTime.now().minusDays(5));
inUserLoginGoToDemoPatientChart();
expectVisible(viewThat(
hasAncestorThat(hasId(R.id.attribute_admission_days)),
hasText("Day 6")));
screenshot("Patient Chart");
}*/
/**
* Tests that the patient chart shows the correct symptoms onset date.
* TODO/completeness: Currently disabled. Re-enable once date picker
* selection works (supposedly works in Espresso 2.0).
*/
/*public void testPatientChart_ShowsCorrectSymptomsOnsetDate() {
inUserLoginGoToDemoPatientChart();
expectVisible(viewThat(
hasAncestorThat(hasId(R.id.attribute_symptoms_onset_days)),
hasText("Day 8")));
screenshot("Patient Chart");
}*/
/**
* Tests that the patient chart shows all days, even when no observations are present.
* TODO/completeness: Currently disabled. Re-enable once date picker
* selection works (supposedly works in Espresso 2.0).
*/
/*public void testPatientChart_ShowsAllDaysInChartWhenNoObservations() {
inUserLoginGoToDemoPatientChart();
expectVisibleWithin(5000, viewThat(hasTextContaining("Today (Day 6)")));
screenshot("Patient Chart");
}*/
// TODO/completeness: Disabled as there seems to be no easy way of
// scrolling correctly with no adapter view.
/** Tests that encounter time can be set to a date in the past and still displayed correctly. */
/*public void testCanSubmitObservationsInThePast() {
inUserLoginGoToDemoPatientChart();
openEncounterForm();
selectDateFromDatePicker("2015", "Jan", null);
answerTextQuestion("Temperature", "29.1");
saveForm();
checkObservationValueEquals(0, "29.1", "1 Jan"); // Temperature
}*/
protected void openEncounterForm() {
// Wait until the overflow menu button is available.
expectVisibleSoon(Espresso.onView(OVERFLOW_BUTTON_MATCHER));
openActionBarOptionsMenu();
EventBusIdlingResource<FetchXformSucceededEvent> xformIdlingResource =
new EventBusIdlingResource<>(UUID.randomUUID().toString(), mEventBus);
ViewInteraction testForm = viewWithText("[test] Form");
expectVisibleSoon(testForm);
click(testForm);
Espresso.registerIdlingResources(xformIdlingResource);
// Give the form time to be parsed on the client (this does not result in an event firing).
expectVisibleSoon(viewWithText("Encounter"));
}
/** Tests that dismissing a form immediately closes it if no changes have been made. */
public void testDismissButtonReturnsImmediatelyWithNoChanges() {
inUserLoginGoToDemoPatientChart();
openEncounterForm();
click(viewWithText("Discard"));
}
/** Tests that dismissing a form results in a dialog if changes have been made. */
public void testDismissButtonShowsDialogWithChanges() {
inUserLoginGoToDemoPatientChart();
openEncounterForm();
answerTextQuestion("Temperature", "29.2");
// Try to discard and give up.
click(viewWithText("Discard"));
expectVisible(viewWithText(R.string.title_discard_observations));
click(viewWithText(R.string.no));
// Try to discard and actually go back.
click(viewWithText("Discard"));
expectVisible(viewWithText(R.string.title_discard_observations));
click(viewWithText(R.string.yes));
}
private void answerTextQuestion(String questionText, String answerText) {
scrollToAndType(answerText, viewThat(
isA(EditText.class),
hasSiblingThat(
isA(MediaLayout.class),
hasDescendantThat(hasTextContaining(questionText)))));
}
private void answerSingleCodedQuestion(String questionText, String answerText) {
answerCodedQuestion(questionText, answerText, ButtonsSelectOneWidget.class,
TableWidgetGroup.class);
}
private void answerMultipleCodedQuestion(String questionText, String answerText) {
answerCodedQuestion(questionText, answerText, ButtonsSelectOneWidget.class,
TableWidgetGroup.class, ODKView.class);
}
private void answerCodedQuestion(String questionText, String answerText,
final Class<? extends View>... classes) {
// Close the soft keyboard before answering any toggle questions -- on rare occasions,
// if Espresso answers one of these questions and is then instructed to type into another
// field, the input event will actually be generated as the keyboard is hiding and will be
// lost, but Espresso won't detect this case.
Espresso.closeSoftKeyboard();
scrollToAndClick(viewThat(
isAnyOf(CheckBox.class, RadioButton.class),
hasAncestorThat(
isAnyOf(classes),
hasDescendantThat(hasTextContaining(questionText))),
hasTextContaining(answerText)));
}
private void saveForm() {
IdlingResource xformWaiter = getXformSubmissionIdlingResource();
click(viewWithText("Save"));
Espresso.registerIdlingResources(xformWaiter);
}
private IdlingResource getXformSubmissionIdlingResource() {
return new EventBusIdlingResource<SubmitXformSucceededEvent>(
UUID.randomUUID().toString(),
mEventBus);
}
/**
* Tests that, when multiple encounters for the same encounter time are submitted within a short
* period of time, that only the latest encounter is present in the relevant column.
*/
public void testEncounter_latestEncounterIsAlwaysShown() {
inUserLoginGoToDemoPatientChart();
// Update a vital tile (pulse) as well as a couple of observations (temperature, vomiting
// count), and verify that the latest value is visible for each.
for (int i = 0; i < 3; i++) {
openEncounterForm();
String temp = Integer.toString(i + 35) + ".7";
String respiratoryRate = Integer.toString(i + 80);
String bpSystolic = Integer.toString(i + 80);
String bpDiastolic = Integer.toString(i + 100);
answerTextQuestion("Temperature", temp);
answerTextQuestion("Respiratory rate", respiratoryRate);
answerTextQuestion("Blood pressure, systolic", bpSystolic);
answerTextQuestion("Blood pressure, diastolic", bpDiastolic);
saveForm();
waitForProgressFragment();
// TODO: implement IdlingResource for webview to remove this sleep.
// Wait a bit for the chart to update it's values.
try{
Thread.sleep(5000);
} catch (InterruptedException ignored){}
//checkVitalValueContains("Pulse", pulse);
checkObservationValueEquals("[test] Temperature (°C)", temp);
checkObservationValueEquals("[test] Respiratory rate (bpm)", respiratoryRate);
checkObservationValueEquals("[test] Blood pressure, systolic", bpSystolic);
checkObservationValueEquals("[test] Blood pressure, diastolic", bpDiastolic);
}
}
private void checkVitalValueContains(String vitalName, String vitalValue) {
// Check for updated vital view.
expectVisibleSoon(viewThat(
hasTextContaining(vitalValue),
hasSiblingThat(hasTextContaining(vitalName))));
}
//TODO: check the todo bellow and remove this commented method
// private void checkObservationValueEquals(int row, String value, String dateKey) {
// // TODO/completeness: actually check dateKey
//
// scrollToAndExpectVisible(viewThat(
// hasText(value),
// hasAncestorThat(isInRow(row, ROW_HEIGHT))));
// }
/**
* Look for the informed value on the last cell of the Observation named row.
* @param obsName the class name added to the tr where the value is. The class name is the
* name of the observation with all non alphanumeric chars replaced by "_".
* @param value the text inside the table cell.
*/
private void checkObservationValueEquals(String obsName, String value) {
String cssSelector = "tr." + Utils.removeUnsafeChars(obsName) + " td:last-child";
onWebView()
.withElement(findElement(Locator.CSS_SELECTOR, cssSelector))
.check(webMatches(getText(), containsString(value)));
}
/** Ensures that non-overlapping observations for the same encounter are combined. */
public void testCombinesNonOverlappingObservationsForSameEncounter() {
inUserLoginGoToDemoPatientChart();
waitForProgressFragment();
// Enter first set of observations for this encounter.
openEncounterForm();
answerTextQuestion("Temperature", "36.5");
answerTextQuestion("Respiratory rate", "23");
answerTextQuestion("oxygen sat", "95");
answerTextQuestion("Blood pressure, systolic", "80");
answerTextQuestion("Blood pressure, diastolic", "100");
saveForm();
// Enter second set of observations for this encounter.
waitForProgressFragment();
openEncounterForm();
answerTextQuestion("Weight", "80.4");
answerTextQuestion("Height", "170");
answerSingleCodedQuestion("Shock", "Mild");
answerSingleCodedQuestion("Consciousness", "Responds to voice");
answerMultipleCodedQuestion("Other symptoms", "Cough");
saveForm();
// Enter third set of observations for this encounter.
waitForProgressFragment();
openEncounterForm();
answerSingleCodedQuestion("Hiccups", "No");
answerSingleCodedQuestion("Headache", "No");
answerSingleCodedQuestion("Sore throat", "Yes");
answerSingleCodedQuestion("Heartburn", "No");
answerSingleCodedQuestion("Pregnant", "Yes");
answerSingleCodedQuestion("Condition", "Unwell");
answerTextQuestion("Notes", "Call family");
saveForm();
// Check that all values are now visible.
waitForProgressFragment();
// Expect a WebView with JS enabled to be visible soon (the chart).
expectVisibleSoon(viewThat(isJavascriptEnabled()));
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
checkObservationValueEquals("[test] Temperature (°C)", "36.5");
checkObservationValueEquals("[test] Respiratory rate (bpm)", "23");
checkObservationValueEquals("[test] SpO₂ oxygen sat (%)", "95");
checkObservationValueEquals("[test] Blood pressure, systolic", "80");
checkObservationValueEquals("[test] Blood pressure, diastolic", "100");
checkObservationValueEquals("[test] Weight (kg)", "80.4");
checkObservationValueEquals("[test] Height (cm)", "170");
checkObservationValueEquals("[test] Shock", "Mild");
checkObservationValueEquals("[test] Consciousness (AVPU)", "V");
checkObservationValueEquals("[test] Cough", YES);
checkObservationValueEquals("[test] Hiccups", NO);
checkObservationValueEquals("[test] Headache", NO);
checkObservationValueEquals("[test] Sore throat", YES);
checkObservationValueEquals("Condition", "2");
checkObservationValueEquals("[test] Notes", "Call …");
}
private void checkObservationSet(int row, String dateKey) {
// TODO/completeness: actually check dateKey
scrollToAndExpectVisible(viewThat(
hasAncestorThat(isInRow(row, ROW_HEIGHT)),
hasBackground(getActivity().getResources().getDrawable(R.drawable.chart_cell_active))));
}
/** Exercises all fields in the encounter form, except for encounter time. */
public void testEncounter_allFieldsWorkOtherThanEncounterTime() {
inUserLoginGoToDemoPatientChart();
waitForProgressFragment();
openEncounterForm();
answerTextQuestion("Temperature", "36.5");
answerTextQuestion("Respiratory rate", "23");
answerTextQuestion("oxygen sat", "95");
answerTextQuestion("Blood pressure, systolic", "80");
answerTextQuestion("Blood pressure, diastolic", "100");
answerTextQuestion("Weight", "80.5");
answerTextQuestion("Height", "170");
answerSingleCodedQuestion("Shock", "Severe");
answerSingleCodedQuestion("Consciousness", "Unresponsive");
answerMultipleCodedQuestion("Other symptoms", "Gingivitis");
answerSingleCodedQuestion("Hiccups", "Unknown");
answerSingleCodedQuestion("Headache", "Yes");
answerSingleCodedQuestion("Sore throat", "No");
answerSingleCodedQuestion("Heartburn", "Yes");
answerSingleCodedQuestion("Pregnant", "No");
answerSingleCodedQuestion("Condition", "Confirmed Dead");
answerTextQuestion("Notes", "Possible malaria.");
saveForm();
waitForProgressFragment();
// TODO: implement IdlingResource for webview to remove this sleep.
// Wait for webview to reload and scripts to run
try{
Thread.sleep(30000);
} catch (InterruptedException e){}
checkObservationValueEquals("[test] Temperature (°C)", "36.5");
checkObservationValueEquals("[test] Respiratory rate (bpm)", "23");
checkObservationValueEquals("[test] SpO₂ oxygen sat (%)", "95");
checkObservationValueEquals("[test] Blood pressure, systolic", "80");
checkObservationValueEquals("[test] Blood pressure, diastolic", "100");
checkObservationValueEquals("[test] Weight (kg)", "80.5");
checkObservationValueEquals("[test] Height (cm)", "170");
checkObservationValueEquals("[test] Shock", "Severe");
checkObservationValueEquals("[test] Consciousness (AVPU)", "U");
checkObservationValueEquals("[test] Gingivitis", YES);
checkObservationValueEquals("[test] Hiccups", NO);
checkObservationValueEquals("[test] Headache", YES);
checkObservationValueEquals("[test] Sore throat", NO);
checkObservationValueEquals("Condition", "6");
checkObservationValueEquals("[test] Notes", "Possi…");
/*
TODO: for now tests are not checking Vital values. We will implement a Test profile to correct this.
checkVitalValueContains("Pulse", "80");
checkVitalValueContains("Respiration", "20");
checkVitalValueContains("Consciousness", "Responds to voice");
checkVitalValueContains("Mobility", "Assisted");
checkVitalValueContains("Diet", "Fluids");
checkVitalValueContains("Hydration", "Needs ORS");
checkVitalValueContains("Condition", "5");
checkVitalValueContains("Pain level", "Severe");
*/
}
}