diff --git a/docs/e2e-testing.md b/docs/e2e-testing.md index d083a936516..97e8d1f63f9 100644 --- a/docs/e2e-testing.md +++ b/docs/e2e-testing.md @@ -49,6 +49,7 @@ Before running tests, modify `src/e2e/resources/test.properties` if necessary, e * If you are planning to test against a production server, specify the path to Chrome's user data directory in `test.chrome.userdata.path` value in `test.properties`. * This is used to bypass login by using previous login data. + * You can enter `chrome://version` into Chrome address bar to identify the profile path (specified under `Profile Path`). * The chromedriver process started by the test suite will not automatically get killed after the tests have finished executing.
You will need to manually kill these processes after the tests are done. @@ -71,23 +72,22 @@ Any individual E2E test | `./gradlew e2eTestTry1 --tests TestClassName` | `{proj If you are testing against a production server (staging server or live server), some additional tasks need to be done. +1. Edit `src/e2e/resources/test.properties` as instructed is in its comments. + * In particular, you will need a legitimate Gmail account to be used for testing. + 1. You need to setup a `Gmail API`1 as follows: - * [Obtain a Gmail API credentials](https://github.com/TEAMMATES/teammates-ops/blob/master/platform-guide.md) and download it. + * [Obtain a Gmail API credentials](https://github.com/TEAMMATES/teammates-ops/blob/master/platform-guide.md#setting-up-gmail-api-credentials) and download it. * Copy the file to `src/e2e/resources/gmail-api` (create the `gmail-api` folder) of your project and rename it to `client_secret.json`. * It is also possible to use the Gmail API credentials from any other Google Cloud Platform project for this purpose. - -1. Edit `src/e2e/resources/test.properties` as instructed is in its comments. - * In particular, you will need a legitimate Gmail account to be used for testing. + * Run `EmailAccountTest` to confirm that the setup works. For the first run, it is expected that you will need to grant access from the test Gmail account to the above API. 1. Login manually to TEAMMATES on the browser used for testing to add cookie with login details to the browser profile. * This profile will be added to the web driver so that E2E tests will start with user already logged in. * This is required as Google does not allow login by automated software. -1. For Firefox, run the full test suite or any subset of it as how you would have done it in dev server. +1. Run the full test suite or any subset of it as how you would have done it in dev server. * Do note that the GAE daily quota is usually not enough to run the full test suite, in particular for accounts with no billing enabled. -1. For Chrome, you may have to run tests one at a time as multiple ChromeDriver instances cannot be opened with the same user data. - 1 This setup is necessary because our test suite uses the Gmail API to access the Gmail account used for testing (the account is specified in `test.properties`) to confirm that the account receives the expected emails from TEAMMATES. This is needed only when testing against a production server because no actual emails are sent by the dev server and therefore delivery of emails is not tested when testing against the dev server. diff --git a/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java index 834c849b783..150406ecb1b 100644 --- a/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java @@ -13,6 +13,7 @@ import teammates.common.util.Const; import teammates.common.util.StringHelper; import teammates.e2e.pageobjects.AdminSearchPage; +import teammates.e2e.util.TestProperties; /** * SUT: {@link Const.WebPageURIs#ADMIN_SEARCH_PAGE}. @@ -237,8 +238,10 @@ private void verifyLinkExpansionButtons(StudentAttributes student, InstructorAtt } private void verifyRegenerateStudentCourseLinks(WebElement studentRow, String originalJoinLink) { - searchPage.verifyStatusMessage("Student's links for this course have been successfully regenerated," - + " and the email has been sent."); + if (TestProperties.isDevServer() || TestProperties.INCLUDE_EMAIL_VERIFICATION) { + searchPage.verifyStatusMessage("Student's links for this course have been successfully " + + "regenerated, and the email has been sent."); + } String regeneratedJoinLink = searchPage.getStudentJoinLink(studentRow); assertNotEquals(regeneratedJoinLink, originalJoinLink); diff --git a/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java b/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java index 9e5dfe3471d..6e6d89fbc29 100644 --- a/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java +++ b/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java @@ -8,6 +8,7 @@ import org.testng.ITestContext; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeSuite; import teammates.common.datatransfer.DataBundle; import teammates.common.datatransfer.attributes.AccountAttributes; @@ -44,10 +45,19 @@ public abstract class BaseE2ETestCase extends BaseTestCaseWithDatastoreAccess { static final BackDoor BACKDOOR = BackDoor.getInstance(); + private static Browser sharedBrowser; protected Browser browser; protected DataBundle testData; + @BeforeSuite + protected void determineEnvironment(ITestContext context) { + if (!TestProperties.isDevServer()) { + // If testing against production server, run in single thread only + context.getSuite().getXmlSuite().setThreadCount(1); + } + } + @BeforeClass public void baseClassSetup() throws Exception { prepareTestData(); @@ -55,7 +65,16 @@ public void baseClassSetup() throws Exception { } protected void prepareBrowser() { - browser = new Browser(); + if (TestProperties.isDevServer()) { + browser = new Browser(); + } else { + // As the tests are run in single thread, in order to reduce the time wasted on browser setup/teardown, + // use a single browser instance for all tests in the suite + if (sharedBrowser == null) { + sharedBrowser = new Browser(); + } + browser = sharedBrowser; + } } protected abstract void prepareTestData() throws Exception; @@ -83,6 +102,9 @@ protected void releaseBrowser(boolean isSuccess) { if (browser == null) { return; } + if (!TestProperties.isDevServer()) { + return; + } if (isSuccess || TestProperties.CLOSE_BROWSER_ON_FAILURE) { browser.driver.close(); } @@ -189,7 +211,7 @@ protected void verifyDownloadedFile(String expectedFileName, List expect * Email used must be an authentic gmail account. */ protected void verifyEmailSent(String email, String subject) { - if (TestProperties.isDevServer()) { + if (TestProperties.isDevServer() || !TestProperties.INCLUDE_EMAIL_VERIFICATION) { return; } if (!TestProperties.TEST_EMAIL.equals(email)) { @@ -199,13 +221,12 @@ protected void verifyEmailSent(String email, String subject) { try { emailAccount.getUserAuthenticated(); int retryLimit = 5; - boolean actual = emailAccount.isEmailWithSubjectPresent(subject); + boolean actual = emailAccount.isRecentEmailWithSubjectPresent(subject, TestProperties.TEST_SENDER_EMAIL); while (!actual && retryLimit > 0) { retryLimit--; ThreadHelper.waitFor(1000); - actual = emailAccount.isEmailWithSubjectPresent(subject); + actual = emailAccount.isRecentEmailWithSubjectPresent(subject, TestProperties.TEST_SENDER_EMAIL); } - emailAccount.markAllUnreadEmailAsRead(); assertTrue(actual); } catch (Exception e) { fail("Failed to verify email sent:" + e); diff --git a/src/e2e/java/teammates/e2e/cases/BaseFeedbackQuestionE2ETest.java b/src/e2e/java/teammates/e2e/cases/BaseFeedbackQuestionE2ETest.java index a3bdebbb135..134e3ce703f 100644 --- a/src/e2e/java/teammates/e2e/cases/BaseFeedbackQuestionE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/BaseFeedbackQuestionE2ETest.java @@ -35,26 +35,20 @@ protected InstructorFeedbackEditPage loginToFeedbackEditPage() { } protected FeedbackSubmitPage loginToFeedbackSubmitPage() { - AppUrl url = createUrl(Const.WebPageURIs.SESSION_SUBMISSION_PAGE) + AppUrl url = createUrl(Const.WebPageURIs.STUDENT_SESSION_SUBMISSION_PAGE) .withUserId(student.googleId) .withCourseId(student.course) .withSessionName(feedbackSession.getFeedbackSessionName()); - FeedbackSubmitPage submitPage = loginAdminToPage(url, FeedbackSubmitPage.class); - submitPage.reloadPageIfStuckLoading(); - - return submitPage; + return loginAdminToPage(url, FeedbackSubmitPage.class); } protected FeedbackSubmitPage getFeedbackSubmitPage() { - AppUrl url = createUrl(Const.WebPageURIs.SESSION_SUBMISSION_PAGE) + AppUrl url = createUrl(Const.WebPageURIs.STUDENT_SESSION_SUBMISSION_PAGE) .withUserId(student.googleId) .withCourseId(student.course) .withSessionName(feedbackSession.getFeedbackSessionName()); - FeedbackSubmitPage submitPage = AppPage.getNewPageInstance(browser, url, FeedbackSubmitPage.class); - submitPage.reloadPageIfStuckLoading(); - - return submitPage; + return AppPage.getNewPageInstance(browser, url, FeedbackSubmitPage.class); } } diff --git a/src/e2e/java/teammates/e2e/cases/FeedbackSubmitPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/FeedbackSubmitPageE2ETest.java index 61cd61d3c7f..54f11b71a82 100644 --- a/src/e2e/java/teammates/e2e/cases/FeedbackSubmitPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/FeedbackSubmitPageE2ETest.java @@ -184,7 +184,7 @@ public void testAll() { } private AppUrl getStudentSubmitPageUrl(StudentAttributes student, FeedbackSessionAttributes session) { - return createUrl(Const.WebPageURIs.SESSION_SUBMISSION_PAGE) + return createUrl(Const.WebPageURIs.STUDENT_SESSION_SUBMISSION_PAGE) .withUserId(student.googleId) .withCourseId(student.course) .withSessionName(session.getFeedbackSessionName()); diff --git a/src/e2e/java/teammates/e2e/cases/InstructorCourseDetailsPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/InstructorCourseDetailsPageE2ETest.java index e04f5707cb1..648cd69d429 100644 --- a/src/e2e/java/teammates/e2e/cases/InstructorCourseDetailsPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/InstructorCourseDetailsPageE2ETest.java @@ -96,7 +96,7 @@ public void testAll() { detailsPage.verifyStatusMessage("An email has been sent to " + student.getEmail()); String expectedEmailSubject = "TEAMMATES: Invitation to join course [" - + course.getName() + "][" + course.getId() + "]"; + + course.getName() + "][Course ID: " + course.getId() + "]"; verifyEmailSent(student.getEmail(), expectedEmailSubject); ______TS("remind all students to join"); diff --git a/src/e2e/java/teammates/e2e/cases/InstructorCourseStudentDetailsEditPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/InstructorCourseStudentDetailsEditPageE2ETest.java index 68a8376b276..518d3193861 100644 --- a/src/e2e/java/teammates/e2e/cases/InstructorCourseStudentDetailsEditPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/InstructorCourseStudentDetailsEditPageE2ETest.java @@ -63,7 +63,9 @@ public void testAll() { student.googleId = null; editPage.editStudentEmailAndResendLinks(newEmail); - editPage.verifyStatusMessage("Student has been updated and email sent"); + if (TestProperties.isDevServer() || TestProperties.INCLUDE_EMAIL_VERIFICATION) { + editPage.verifyStatusMessage("Student has been updated and email sent"); + } verifyPresentInDatastore(student); verifyEmailSent(newEmail, "TEAMMATES: Summary of course [" + course.getName() + "][Course ID: " + course.getId() + "]"); diff --git a/src/e2e/java/teammates/e2e/cases/InstructorHomePageE2ETest.java b/src/e2e/java/teammates/e2e/cases/InstructorHomePageE2ETest.java index 0078d5bd234..1f2f76b2d51 100644 --- a/src/e2e/java/teammates/e2e/cases/InstructorHomePageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/InstructorHomePageE2ETest.java @@ -73,6 +73,7 @@ public void testAll() { ______TS("search for valid student"); InstructorSearchPage searchPage = homePage.searchKeyword(studentToEmail.getName()); + searchPage.waitForPageToLoad(); // Here, it is sufficient to ensure that the number of search results matches // A more thorough testing of this page will be done in its own E2E test @@ -82,6 +83,7 @@ public void testAll() { ______TS("search for invalid student"); searchPage = homePage.searchKeyword("INVALID"); + searchPage.waitForPageToLoad(true); searchPage.verifyStatusMessage("No results found."); searchPage.verifyNumCoursesInStudentResults(0); diff --git a/src/e2e/java/teammates/e2e/cases/StudentFeedbackResultsPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/StudentFeedbackResultsPageE2ETest.java index 6babc47149e..b2e2ab07031 100644 --- a/src/e2e/java/teammates/e2e/cases/StudentFeedbackResultsPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/StudentFeedbackResultsPageE2ETest.java @@ -51,7 +51,6 @@ public void testAll() { .withStudentEmail(unregistered.email) .withSessionName(openSession.getFeedbackSessionName()) .withRegistrationKey(getKeyForStudent(unregistered)); - logout(); resultsPage = AppPage.getNewPageInstance(browser, url, StudentFeedbackResultsPage.class); resultsPage.verifyFeedbackSessionDetails(openSession); diff --git a/src/e2e/java/teammates/e2e/cases/StudentHomePageE2ETest.java b/src/e2e/java/teammates/e2e/cases/StudentHomePageE2ETest.java index ccbed4e7cdd..78413444694 100644 --- a/src/e2e/java/teammates/e2e/cases/StudentHomePageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/StudentHomePageE2ETest.java @@ -45,8 +45,6 @@ public void testAll() { assertTrue(verifyVisibleFeedbackSessionToStudents(feedbackSessionName, i)); } - - logout(); } private List getStudentHomeCoursePanels() { diff --git a/src/e2e/java/teammates/e2e/cases/TimezoneSyncerTest.java b/src/e2e/java/teammates/e2e/cases/TimezoneSyncerTest.java index cb6baf224a0..141dcdf05cd 100644 --- a/src/e2e/java/teammates/e2e/cases/TimezoneSyncerTest.java +++ b/src/e2e/java/teammates/e2e/cases/TimezoneSyncerTest.java @@ -76,8 +76,6 @@ public void testAll() { releaseDate.plusDays(DAYS_TO_UPDATE_TZ).isAfter(nowDate)); } - - logout(); } private String processOffsets(String offsets) { diff --git a/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java b/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java index 8a5aeeede3b..46dc71f60af 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java @@ -75,7 +75,6 @@ public void clickSearchButton() { public void regenerateLinksForStudent(StudentAttributes student) { WebElement studentRow = getStudentRow(student); studentRow.findElement(By.xpath("//button[text()='Regenerate links']")).click(); - waitForPageToLoad(); waitForConfirmationModalAndClickOk(); waitForPageToLoad(true); @@ -153,11 +152,11 @@ public String getStudentJoinLink(WebElement studentRow) { public void resetStudentGoogleId(StudentAttributes student) { WebElement studentRow = getStudentRow(student); - studentRow.findElement(By.linkText(LINK_TEXT_RESET_GOOGLE_ID)).click(); - waitForPageToLoad(); + WebElement link = studentRow.findElement(By.linkText(LINK_TEXT_RESET_GOOGLE_ID)); + link.click(); waitForConfirmationModalAndClickOk(); - waitForPageToLoad(); + waitForElementStaleness(link); } public WebElement getInstructorRow(InstructorAttributes instructor) { @@ -201,11 +200,11 @@ public String getInstructorJoinLink(WebElement instructorRow) { public void resetInstructorGoogleId(InstructorAttributes instructor) { WebElement instructorRow = getInstructorRow(instructor); - instructorRow.findElement(By.linkText(LINK_TEXT_RESET_GOOGLE_ID)).click(); - waitForPageToLoad(); + WebElement link = instructorRow.findElement(By.linkText(LINK_TEXT_RESET_GOOGLE_ID)); + link.click(); waitForConfirmationModalAndClickOk(); - waitForPageToLoad(); + waitForElementStaleness(link); } public int getNumExpandedRows(WebElement row) { diff --git a/src/e2e/java/teammates/e2e/pageobjects/AppPage.java b/src/e2e/java/teammates/e2e/pageobjects/AppPage.java index 0b72b8f7cce..864a7dd69a6 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AppPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AppPage.java @@ -13,7 +13,6 @@ import java.util.Map; import org.openqa.selenium.By; -import org.openqa.selenium.Dimension; import org.openqa.selenium.InvalidElementStateException; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.Keys; @@ -183,9 +182,13 @@ public void waitForElementToBeClickable(WebElement element) { } public void waitUntilAnimationFinish() { - WebDriverWait wait = new WebDriverWait(browser.driver, 2); + WebDriverWait wait = new WebDriverWait(browser.driver, 3); wait.until(ExpectedConditions.invisibilityOfElementLocated(By.className("ng-animating"))); - ThreadHelper.waitFor(500); + ThreadHelper.waitFor(1000); + } + + public void waitForLoadingElement() { + waitForElementStaleness(waitForElementPresence(By.className("loading-container"))); } /** @@ -251,17 +254,6 @@ public void reloadPage() { waitForPageToLoad(); } - public void reloadPageIfStuckLoading() { - By loadingContainer = By.className("loading-container"); - try { - if (isElementPresent(loadingContainer)) { - waitForElementStaleness(browser.driver.findElement(loadingContainer)); - } - } catch (TimeoutException e) { - reloadPage(); - } - } - protected Object executeScript(String script, Object... args) { JavascriptExecutor javascriptExecutor = (JavascriptExecutor) browser.driver; return javascriptExecutor.executeScript(script, args); @@ -653,14 +645,6 @@ public void run() { } } - /** - * Set browser window to x width and y height. - */ - protected void setWindowSize(int x, int y) { - Dimension d = new Dimension(x, y); - browser.driver.manage().window().setSize(d); - } - /** * Switches to the new browser window just opened. */ diff --git a/src/e2e/java/teammates/e2e/pageobjects/Browser.java b/src/e2e/java/teammates/e2e/pageobjects/Browser.java index 1fa194136b9..f52f2026ba1 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/Browser.java +++ b/src/e2e/java/teammates/e2e/pageobjects/Browser.java @@ -144,7 +144,6 @@ private WebDriver createWebDriver() { FirefoxProfile profile; if (TestProperties.isDevServer()) { profile = new FirefoxProfile(); - profile.setPreference("browser.private.browsing.autostart", true); } else { // Get user data from browser to bypass google blocking automated log in. // Log in manually to teammates to use that log in data for e2e tests. @@ -165,6 +164,10 @@ private WebDriver createWebDriver() { profile.setPreference("browser.download.dir", downloadPath); FirefoxOptions options = new FirefoxOptions().setProfile(profile); + if (TestProperties.isDevServer()) { + options.addArguments("-private"); + } + return new FirefoxDriver(options); } @@ -174,7 +177,7 @@ private WebDriver createWebDriver() { Map chromePrefs = new HashMap<>(); chromePrefs.put("download.default_directory", downloadPath); - chromePrefs.put("profile.default_content_settings.popups", 0); + chromePrefs.put("download.prompt_for_download", false); ChromeOptions options = new ChromeOptions(); options.setExperimentalOption("prefs", chromePrefs); options.addArguments("--allow-file-access-from-files"); diff --git a/src/e2e/java/teammates/e2e/pageobjects/FeedbackSubmitPage.java b/src/e2e/java/teammates/e2e/pageobjects/FeedbackSubmitPage.java index c935834479b..6f2e8cb36e6 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/FeedbackSubmitPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/FeedbackSubmitPage.java @@ -721,13 +721,13 @@ private int getRecipientIndex(int qnNumber, String recipient) { } catch (NoSuchElementException e) { // continue } - int i = 0; - while (true) { + int limit = 20; // we are not likely to set test data exceeding this number + for (int i = 0; i < limit; i++) { if (questionForm.findElement(By.id("recipient-name-" + i)).getText().contains(recipient)) { return i; } - i++; } + return -1; } private WebElement getTextResponseEditor(int qnNumber, String recipient) { diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEditPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEditPage.java index e4621310ed5..f48b2b2d3dc 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEditPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEditPage.java @@ -300,6 +300,7 @@ private void clickAddNewInstructorButton() { private void clickEditInstructorButton(int instrNum) { click(getEditInstructorButton(instrNum)); + waitUntilAnimationFinish(); } private void clickCancelInstructorButton(int instrNum) { diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEnrollPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEnrollPage.java index 8f0bd6211a4..651c70ceb12 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEnrollPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEnrollPage.java @@ -132,8 +132,8 @@ public void verifyResultsPanelContains(StudentAttributes[] expectedNewStudents, private void fillEnrollSpreadsheet(String[][] expectedStudentData) { WebElement firstCell = getEnrollSpreadsheetFirstCell(); + scrollElementToCenterAndClick(firstCell); Actions actions = new Actions(browser.driver); - actions.click(firstCell).perform(); for (String[] expectedRowData : expectedStudentData) { for (String expectedCellData : expectedRowData) { actions.sendKeys(expectedCellData + Keys.TAB).perform(); @@ -141,9 +141,7 @@ private void fillEnrollSpreadsheet(String[][] expectedStudentData) { } } - // Does not work if first cell is not visible private WebElement getEnrollSpreadsheetFirstCell() { - setWindowSize(1000, 1000); return enrollSpreadsheet.findElement(By.tagName("tbody")).findElement(By.tagName("td")); } diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java index 2eb795198c1..41fc96049e5 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java @@ -37,15 +37,6 @@ public class InstructorCoursesPage extends AppPage { @FindBy(id = "btn-save-course") private WebElement submitButton; - @FindBy(id = "sort-course-name") - private WebElement sortByCourseNameIcon; - - @FindBy (id = "sort-course-id") - private WebElement sortByCourseIdIcon; - - @FindBy (id = "sort-creation-date") - private WebElement sortByCreationDateIcon; - @FindBy(id = "active-courses-table") private WebElement activeCoursesTable; @@ -220,15 +211,15 @@ public void deleteAllCourses() { } public void sortByCourseName() { - click(sortByCourseNameIcon); + click(waitForElementPresence(By.id("sort-course-name"))); } public void sortByCourseId() { - click(sortByCourseIdIcon); + click(waitForElementPresence(By.id("sort-course-id"))); } public void sortByCreationDate() { - click(sortByCreationDateIcon); + click(waitForElementPresence(By.id("sort-creation-date"))); } private WebElement getActiveTableRow(String courseId) { diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackEditPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackEditPage.java index a786e4c8d3a..81781bd584a 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackEditPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackEditPage.java @@ -50,9 +50,6 @@ public class InstructorFeedbackEditPage extends AppPage { @FindBy(id = "btn-fs-save") private WebElement fsSaveButton; - @FindBy(id = "btn-fs-delete") - private WebElement fsDeleteButton; - @FindBy(id = "btn-fs-copy") private WebElement fsCopyButton; @@ -290,7 +287,7 @@ public void copySessionToOtherCourse(CourseAttributes otherCourse, String sessio } public void deleteSession() { - clickAndConfirm(fsDeleteButton); + clickAndConfirm(waitForElementPresence(By.id("btn-fs-delete"))); } public FeedbackSubmitPage previewAsStudent(StudentAttributes student) { @@ -1143,7 +1140,12 @@ private void clickAndWaitForNewQuestion(WebElement button) { private void addNewQuestion(int optionNumber) { click(addNewQuestionButton); WebElement newQuestionDropdown = waitForElementPresence(By.id("new-question-dropdown")); - click(newQuestionDropdown.findElements(By.tagName("button")).get(optionNumber - 1)); + WebElement optionButton = newQuestionDropdown.findElements(By.tagName("button")).get(optionNumber - 1); + if (optionNumber == 1) { + click(optionButton); + } else { + clickAndWaitForNewQuestion(optionButton); + } } private void clickSaveNewQuestionButton() { @@ -1514,12 +1516,12 @@ private void inputRubricDetails(int questionNum, FeedbackRubricQuestionDetails q List textAreas = getRubricTextareas(questionNum, i + 2); fillTextBox(textAreas.get(0), subQuestions.get(i)); for (int j = 0; j < numChoices; j++) { + fillTextBox(textAreas.get(j + 1), descriptions.get(i).get(j)); if (descriptions.get(i).get(j).isEmpty()) { - // using clear does not work here - textAreas.get(j + 1).sendKeys(Keys.chord(Keys.CONTROL, "a")); - textAreas.get(j + 1).sendKeys(Keys.DELETE); - } else { - fillTextBox(textAreas.get(j + 1), descriptions.get(i).get(j)); + // using clear does not send the required event + // as a workaround, after clearing without event, enter a random character and delete it + textAreas.get(j + 1).sendKeys("a"); + textAreas.get(j + 1).sendKeys(Keys.BACK_SPACE); } } } diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorHomePage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorHomePage.java index 46c4c02e4ac..43a9bff6cc6 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorHomePage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorHomePage.java @@ -39,7 +39,7 @@ protected boolean containsExpectedPageContents() { public InstructorSearchPage searchKeyword(String keyword) { fillTextBox(searchBar, keyword); click(searchButton); - waitForPageToLoad(true); + waitUntilAnimationFinish(); return changePageType(InstructorSearchPage.class); } diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorSearchPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorSearchPage.java index f6d71608b76..7953dbbacfd 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorSearchPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorSearchPage.java @@ -60,8 +60,7 @@ public void search(boolean searchForStudents, boolean searchForComments, String searchKeyword.clear(); searchKeyword.sendKeys(searchTerm); click(searchButton); - waitForPageToLoad(true); - waitUntilAnimationFinish(); + waitForLoadingElement(); } else { verifyUnclickable(searchButton); } diff --git a/src/e2e/java/teammates/e2e/util/EmailAccount.java b/src/e2e/java/teammates/e2e/util/EmailAccount.java index 58808268787..bde2b9abbb7 100644 --- a/src/e2e/java/teammates/e2e/util/EmailAccount.java +++ b/src/e2e/java/teammates/e2e/util/EmailAccount.java @@ -6,11 +6,9 @@ import java.util.Collections; import java.util.List; import java.util.Properties; +import java.util.concurrent.TimeUnit; -import javax.mail.BodyPart; import javax.mail.MessagingException; -import javax.mail.Multipart; -import javax.mail.Part; import javax.mail.Session; import javax.mail.internet.MimeMessage; @@ -21,8 +19,6 @@ import com.google.api.services.gmail.model.ModifyMessageRequest; import com.google.common.io.BaseEncoding; -import teammates.common.util.EmailType; - /** * Provides an access to real Gmail inbox used for testing. * @@ -47,10 +43,12 @@ public void getUserAuthenticated() throws IOException { // assume user is authenticated before service = new GmailServiceMaker(username).makeGmailService(); - while (true) { + int retryLimit = 5; + while (retryLimit > 0) { try { + retryLimit--; // touch one API endpoint to check authentication - getListOfUnreadEmailOfUser(); + getListOfUnreadEmailFromSender(1L, ""); break; } catch (HttpResponseException e) { if (e.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN @@ -67,53 +65,24 @@ public void getUserAuthenticated() throws IOException { } /** - * Retrieves the registration key among the unread emails - * with {@code courseId} and {@code courseName} sent to the Gmail inbox. - * - *

After retrieving, marks the email as read. - * - *

If multiple emails of the same course are in the inbox, return the registration key presented in one of them. - * - * @return registration key (null if cannot be found). + * Returns true if unread mail that arrived in the past minute contains mail with the specified subject. */ - public String getRegistrationKeyFromUnreadEmails(String courseName, String courseId) + public boolean isRecentEmailWithSubjectPresent(String subject, String senderEmail) throws IOException, MessagingException { - List messageStubs = getListOfUnreadEmailOfUser(); + List messageStubs = getListOfUnreadEmailFromSender(10L, senderEmail); for (Message messageStub : messageStubs) { Message message = service.users().messages().get(username, messageStub.getId()).setFormat("raw") .execute(); MimeMessage email = convertFromMessageToMimeMessage(message); + boolean isSubjectEqual = email.getSubject().equals(subject); + boolean isSentWithinLastMin = + message.getInternalDate() > System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(1); - if (isStudentCourseJoinRegistrationEmail(email, courseName, courseId)) { - String body = getTextFromEmail(email); - + if (isSubjectEqual && isSentWithinLastMin) { markMessageAsRead(messageStub); - - return getKey(body); - } - } - - return null; - } - - /** - * Returns true if unread mail contains mail with the specified subject. - */ - public boolean isEmailWithSubjectPresent(String subject) - throws IOException, MessagingException { - - List messageStubs = getListOfUnreadEmailOfUser(); - - for (Message messageStub : messageStubs) { - Message message = service.users().messages().get(username, messageStub.getId()).setFormat("raw") - .execute(); - - MimeMessage email = convertFromMessageToMimeMessage(message); - - if (email.getSubject().equals(subject)) { return true; } } @@ -121,21 +90,13 @@ public boolean isEmailWithSubjectPresent(String subject) } /** - * Marks all unread emails in the user's inbox as read. + * Returns a list of up to maxResults number of unread emails from the sender. + * Returns an empty list if there is no unread email from sender. */ - public void markAllUnreadEmailAsRead() throws IOException { - List messageStubs = getListOfUnreadEmailOfUser(); - - for (Message messageStub : messageStubs) { - markMessageAsRead(messageStub); - } - } - - /** - * Returns an empty list if there is no unread email of the user. - */ - private List getListOfUnreadEmailOfUser() throws IOException { - List messageStubs = service.users().messages().list(username).setQ("is:UNREAD").execute().getMessages(); + private List getListOfUnreadEmailFromSender(long maxResults, String senderEmail) throws IOException { + List messageStubs = service.users().messages().list(username) + .setQ("is:UNREAD from:" + senderEmail).setMaxResults(maxResults).execute() + .getMessages(); return messageStubs == null ? new ArrayList<>() : messageStubs; } @@ -157,126 +118,6 @@ private MimeMessage convertFromMessageToMimeMessage(Message message) throws Mess return new MimeMessage(session, new ByteArrayInputStream(emailBytes)); } - private boolean isStudentCourseJoinRegistrationEmail(MimeMessage message, String courseName, String courseId) - throws MessagingException { - String subject = message.getSubject(); - return subject != null && subject.equals(String.format(EmailType.STUDENT_COURSE_JOIN.getSubject(), - courseName, courseId)); - } - - /** - * Gets the email message body as text. - */ - private String getTextFromEmail(MimeMessage email) throws MessagingException, IOException { - if (email.isMimeType("text/*")) { - return (String) email.getContent(); - } else { - return getTextFromPart(email); - } - } - - private String getTextFromPart(Part part) throws MessagingException, IOException { - if (part.isMimeType("multipart/alternative")) { - return getTextFromMultiPartAlternative((Multipart) part.getContent()); - } else if (part.isMimeType("multipart/digest")) { - return getTextFromMultiPartDigest((Multipart) part.getContent()); - } else if (mimeTypeCanBeHandledAsMultiPartMixed(part)) { - return getTextHandledAsMultiPartMixed(part); - } - - return null; - } - - /** - * Returns if the part can be handled as multipart/mixed. - */ - private boolean mimeTypeCanBeHandledAsMultiPartMixed(Part part) throws MessagingException { - return part.isMimeType("multipart/mixed") || part.isMimeType("multipart/parallel") - || part.isMimeType("message/rfc822") - // as per the RFC2046 specification, other multipart subtypes are recognized as multipart/mixed - || part.isMimeType("multipart/*"); - } - - private String getTextFromMultiPartDigest(Multipart multipart) throws IOException, MessagingException { - StringBuilder textBuilder = new StringBuilder(); - for (int i = 0; i < multipart.getCount(); i++) { - BodyPart bodyPart = multipart.getBodyPart(i); - if (bodyPart.isMimeType("message/rfc822")) { - String text = getTextFromPart(bodyPart); - if (text != null) { - textBuilder.append(text); - } - } - } - String text = textBuilder.toString(); - - if (text.isEmpty()) { - return null; - } - - return text; - } - - /** - * Returns the text from multipart/alternative, the type of text returned follows the preference of the sending agent. - */ - private String getTextFromMultiPartAlternative(Multipart multipart) throws IOException, MessagingException { - // search in reverse order as a multipart/alternative should have their most preferred format last - for (int i = multipart.getCount() - 1; i >= 0; i--) { - BodyPart bodyPart = multipart.getBodyPart(i); - - if (bodyPart.isMimeType("text/html")) { - return (String) bodyPart.getContent(); - } else if (bodyPart.isMimeType("text/plain")) { - // Since we are looking in reverse order, if we did not encounter a text/html first we can return the plain - // text because that is the best preferred format that we understand. If a text/html comes along later it - // means the agent sending the email did not set the html text as preferable or did not set their preferred - // order correctly, and in that case we do not handle that. - return (String) bodyPart.getContent(); - } else if (bodyPart.isMimeType("multipart/*") || bodyPart.isMimeType("message/rfc822")) { - String text = getTextFromPart(bodyPart); - if (text != null) { - return text; - } - } - } - // we do not know how to handle the text in the multipart or there is no text - return null; - } - - private String getTextHandledAsMultiPartMixed(Part part) throws IOException, MessagingException { - return getTextFromMultiPartMixed((Multipart) part.getContent()); - } - - private String getTextFromMultiPartMixed(Multipart multipart) throws IOException, MessagingException { - StringBuilder textBuilder = new StringBuilder(); - for (int i = 0; i < multipart.getCount(); i++) { - BodyPart bodyPart = multipart.getBodyPart(i); - if (bodyPart.isMimeType("text/*")) { - textBuilder.append((String) bodyPart.getContent()); - } else if (bodyPart.isMimeType("multipart/*")) { - String text = getTextFromPart(bodyPart); - if (text != null) { - textBuilder.append(text); - } - } - } - String text = textBuilder.toString(); - - if (text.isEmpty()) { - return null; - } - - return text; - } - - private String getKey(String body) { - String key = body.substring( - body.indexOf("key=") + "key=".length(), - body.indexOf("studentemail=") - 1); //*If prompted to log in - return key.trim(); - } - public String getUsername() { return username; } diff --git a/src/e2e/java/teammates/e2e/util/EmailAccountTest.java b/src/e2e/java/teammates/e2e/util/EmailAccountTest.java new file mode 100644 index 00000000000..154699725c7 --- /dev/null +++ b/src/e2e/java/teammates/e2e/util/EmailAccountTest.java @@ -0,0 +1,19 @@ +package teammates.e2e.util; + +import org.testng.annotations.Test; + +/** + * Checks that the email account is ready for testing against staging/production server. + */ +public final class EmailAccountTest { + + @Test + public void checkEmailAccount() throws Exception { + if (TestProperties.isDevServer()) { + // Access to actual email account is not necessary for dev server testing + return; + } + new EmailAccount(TestProperties.TEST_EMAIL).getUserAuthenticated(); + } + +} diff --git a/src/e2e/java/teammates/e2e/util/TestProperties.java b/src/e2e/java/teammates/e2e/util/TestProperties.java index 44c7defabb8..4d51dfaddc9 100644 --- a/src/e2e/java/teammates/e2e/util/TestProperties.java +++ b/src/e2e/java/teammates/e2e/util/TestProperties.java @@ -25,6 +25,9 @@ public final class TestProperties { /** The email address used for testing that emails are sent by the system. */ public static final String TEST_EMAIL; + /** The email address used by the system the send emails. */ + public static final String TEST_SENDER_EMAIL; + /** The value of "test.csrf.key" in test.properties file. */ public static final String CSRF_KEY; @@ -62,6 +65,9 @@ public final class TestProperties { /** The value of "test.persistence.timeout" in test.properties file. */ public static final int PERSISTENCE_RETRY_PERIOD_IN_S; + /** The flag to indicate whether emails sent should be verified. */ + public static final boolean INCLUDE_EMAIL_VERIFICATION; + /** The directory where credentials used in Gmail API are stored. */ static final String TEST_GMAIL_API_FOLDER = "src/e2e/resources/gmail-api"; @@ -75,6 +81,7 @@ public final class TestProperties { TEAMMATES_URL = UrlExtension.trimTrailingSlash(prop.getProperty("test.app.url")); TEST_EMAIL = prop.getProperty("test.email"); + TEST_SENDER_EMAIL = prop.getProperty("test.senderemail"); CSRF_KEY = prop.getProperty("test.csrf.key"); BACKDOOR_KEY = prop.getProperty("test.backdoor.key"); @@ -90,6 +97,8 @@ public final class TestProperties { TEST_TIMEOUT = Integer.parseInt(prop.getProperty("test.timeout")); PERSISTENCE_RETRY_PERIOD_IN_S = Integer.parseInt(prop.getProperty("test.persistence.timeout")); + INCLUDE_EMAIL_VERIFICATION = Boolean.parseBoolean(prop.getProperty("test.verify.emails")); + } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/e2e/resources/test.template.properties b/src/e2e/resources/test.template.properties index c8478c5cdbf..09f18dc6f8c 100644 --- a/src/e2e/resources/test.template.properties +++ b/src/e2e/resources/test.template.properties @@ -73,4 +73,16 @@ test.persistence.timeout=16 # The email address below will receive some emails from the production server, # which will be programmatically checked as part of E2E tests. -test.email=alice.tmms@example.tmt +test.email=alice.tmms@gmail.tmt + +# This is the sender email for all emails sent by the system. +# It should have the same value as app.email.senderemail in build.properties +# It is used to filter emails by sender for more efficient verification + +test.senderemail=admin@teammates-john.appspotmail.com + +# Indicates whether emails should be verified during E2E test. +# Please note that GAE has limited free email quota. +# Email verification should be stopped once the quota is depleted. + +test.verify.emails=true diff --git a/src/e2e/resources/test.travis-chrome.properties b/src/e2e/resources/test.travis-chrome.properties index 0b48aacc093..b8ae5feeff3 100644 --- a/src/e2e/resources/test.travis-chrome.properties +++ b/src/e2e/resources/test.travis-chrome.properties @@ -10,4 +10,5 @@ test.browser.closeonfailure=true test.chromedriver.path=/home/travis/chromedriver test.timeout=5 test.persistence.timeout=16 -test.email=alice.tmms@example.tmt +test.email=alice.tmms@gmail.tmt +test.verify.emails=false diff --git a/src/e2e/resources/test.travis.properties b/src/e2e/resources/test.travis.properties index 97f2607d5fe..73b594bc709 100644 --- a/src/e2e/resources/test.travis.properties +++ b/src/e2e/resources/test.travis.properties @@ -11,4 +11,5 @@ test.geckodriver.path=/home/travis/geckodriver test.firefox.path= test.timeout=5 test.persistence.timeout=16 -test.email=alice.tmms@example.tmt +test.email=alice.tmms@gmail.tmt +test.verify.emails=false diff --git a/src/main/java/teammates/common/util/Const.java b/src/main/java/teammates/common/util/Const.java index f4c51fe0b32..538e90bfe03 100644 --- a/src/main/java/teammates/common/util/Const.java +++ b/src/main/java/teammates/common/util/Const.java @@ -620,6 +620,7 @@ public static class WebPageURIs { public static final String STUDENT_HOME_PAGE = STUDENT_PAGE + "/home"; public static final String STUDENT_COURSE_DETAILS_PAGE = STUDENT_PAGE + "/course"; public static final String STUDENT_PROFILE_PAGE = STUDENT_PAGE + "/profile"; + public static final String STUDENT_SESSION_SUBMISSION_PAGE = STUDENT_PAGE + "/sessions/submission"; public static final String STUDENT_SESSION_RESULTS_PAGE = STUDENT_PAGE + "/sessions/result"; public static final String SESSION_RESULTS_PAGE = URI_PREFIX + "/sessions/result";