From d1f1af8f523a9e7c0ce6598d13ed490b50151ebd Mon Sep 17 00:00:00 2001 From: Joe Hoag Date: Wed, 14 Aug 2019 09:05:56 -0600 Subject: [PATCH] [Student][E2E][MBL-12978] | E2E tests for assignments (#234) * MBL-12978: Initial effort towards AssignmentsE2ETest * MBL-12978: More logic for AssignmentsE2E test * MBL-12978: More tweaks * MBL-12978: Addressed PR feedback Removed commented-out println --- .../student/ui/e2e/AssignmentsE2ETest.kt | 174 +++++++++++++++++- .../student/ui/pages/AssignmentDetailsPage.kt | 39 ++++ .../student/ui/pages/AssignmentListPage.kt | 51 ++++- .../student/ui/pages/CourseBrowserPage.kt | 31 ++++ .../student/ui/pages/DashboardPage.kt | 5 + .../student/ui/pages/SubmissionDetailsPage.kt | 36 ++++ .../SubmissionCommentsRenderPage.kt | 11 +- .../student/ui/utils/StudentTest.kt | 4 +- .../res/layout/fragment_course_browser.xml | 3 +- 9 files changed, 347 insertions(+), 7 deletions(-) create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt index dbffd9d45a..6be250d20f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt @@ -1,12 +1,24 @@ package com.instructure.student.ui.e2e +import android.os.SystemClock.sleep +import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.E2E -import com.instructure.canvas.espresso.Stub +import com.instructure.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.api.SubmissionsApi +import com.instructure.dataseeding.model.FileUploadType +import com.instructure.dataseeding.model.GradingType +import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.util.days +import com.instructure.dataseeding.util.fromNow +import com.instructure.dataseeding.util.iso8601 import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.uploadTextFile import org.junit.Test class AssignmentsE2ETest: StudentTest() { @@ -14,11 +26,167 @@ class AssignmentsE2ETest: StudentTest() { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } - @Stub @E2E @Test - @TestMetaData(Priority.P0, FeatureCategory.ASSIGNMENTS, TestCategory.E2E, true) + @TestMetaData(Priority.P0, FeatureCategory.ASSIGNMENTS, TestCategory.E2E) fun testAssignmentsE2E() { + // Seed basic student/teacher/course data + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + // Seed some assignments + val pointsTextAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + gradingType = GradingType.POINTS, + teacherToken = teacher.token, + pointsPossible = 15.0, + dueAt = 1.days.fromNow.iso8601 + )) + + val letterGradeTextAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + gradingType = GradingType.LETTER_GRADE, + teacherToken = teacher.token, + pointsPossible = 20.0 + )) + + val percentageFileAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), + gradingType = GradingType.PERCENT, + teacherToken = teacher.token, + pointsPossible = 25.0, + allowedExtensions = listOf("txt", "pdf", "jpg") + )) + + // Pre-seed a submission and a grade for the letter grade assignment + SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest( + assignmentId = letterGradeTextAssignment.id, + courseId = course.id, + studentToken = student.token, + submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo( + amount = 1, + submissionType = SubmissionType.ONLINE_TEXT_ENTRY + )) + )) + + val submissionGrade = SubmissionsApi.gradeSubmission( + teacherToken = teacher.token, + courseId = course.id, + assignmentId = letterGradeTextAssignment.id, + studentId = student.id, + postedGrade = "16", + excused = false + ) + + + // Sign in with lone student + tokenLogin(student) + + // Go into our course + dashboardPage.waitForRender() + dashboardPage.selectCourse(course) + + // Select the assignments tab + courseBrowserPage.selectAssignments() + + // Verify that our assignments are present, along with any grade/date info + assignmentListPage.assertHasAssignment(pointsTextAssignment) + assignmentListPage.assertHasAssignment(letterGradeTextAssignment, submissionGrade.grade) + assignmentListPage.assertHasAssignment(percentageFileAssignment) + + // Let's submit a text assignment + assignmentListPage.clickAssignment(pointsTextAssignment) + + SubmissionsApi.submitCourseAssignment( + submissionType = SubmissionType.ONLINE_TEXT_ENTRY, + courseId = course.id, + assignmentId = pointsTextAssignment.id, + studentToken = student.token, + fileIds = emptyList().toMutableList() + ) + + assignmentDetailsPage.refresh() + assignmentDetailsPage.verifyAssignmentSubmitted() + + // Let's grade the assignment + val textGrade = SubmissionsApi.gradeSubmission( + teacherToken = teacher.token, + courseId = course.id, + assignmentId = pointsTextAssignment.id, + studentId = student.id, + postedGrade = "13", + excused = false + ) + + assignmentDetailsPage.refresh() + assignmentDetailsPage.verifyAssignmentGraded("13") + + Espresso.pressBack() // Back to assignment list + + // Upload a text file for submission + assignmentListPage.clickAssignment(percentageFileAssignment) + val uploadInfo = uploadTextFile( + courseId = course.id, + assignmentId = percentageFileAssignment.id, + token = student.token, + fileUploadType = FileUploadType.ASSIGNMENT_SUBMISSION + ) + + // Submit the assignment + SubmissionsApi.submitCourseAssignment( + submissionType = SubmissionType.ONLINE_UPLOAD, + courseId = course.id, + assignmentId = percentageFileAssignment.id, + fileIds = listOf(uploadInfo.id).toMutableList(), + studentToken = student.token + ) + + // Verify that assignment has been submitted + assignmentDetailsPage.refresh() + assignmentDetailsPage.verifyAssignmentSubmitted() + + // Grade the assignment + val fileGrade = SubmissionsApi.gradeSubmission( + teacherToken = teacher.token, + courseId = course.id, + assignmentId = percentageFileAssignment.id, + studentId = student.id, + postedGrade = "22", + excused = false + ) + + // Verify that the assignment has been graded + assignmentDetailsPage.refresh() + assignmentDetailsPage.verifyAssignmentGraded("22") + + // Back to assignment list page + Espresso.pressBack() + + // Let's verify that the assignments in the list all have grades now + assignmentListPage.refresh() + assignmentListPage.assertHasAssignment(pointsTextAssignment, textGrade.grade) + assignmentListPage.assertHasAssignment(letterGradeTextAssignment, submissionGrade.grade) + assignmentListPage.assertHasAssignment(percentageFileAssignment, fileGrade.grade) + + // Let's make sure that comments are working + assignmentListPage.clickAssignment(percentageFileAssignment) + assignmentDetailsPage.goToSubmissionDetails() + submissionDetailsPage.openComments() + submissionDetailsPage.assertCommentDisplayed( + uploadInfo.fileName, + student) + + // Add a comment, make sure it shows up in the stream + submissionDetailsPage.addAndSendComment("My comment!!") + sleep(2000) // Give the comment time to propagate + submissionDetailsPage.assertCommentDisplayed( + "My comment!!", + student + ) } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt index ff5fd5173b..5485e6f355 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt @@ -16,8 +16,47 @@ */ package com.instructure.student.ui.pages +import android.view.View +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.assertContainsText +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.click +import com.instructure.espresso.closeSoftKeyboard import com.instructure.espresso.page.BasePage +import com.instructure.espresso.scrollTo +import com.instructure.espresso.swipeDown +import com.instructure.espresso.typeText import com.instructure.student.R +import org.hamcrest.Matcher +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.containsString open class AssignmentDetailsPage : BasePage(R.id.assignmentDetailsPage) { + fun verifyAssignmentSubmitted() { + onView(withText(R.string.submissionStatusSuccessTitle)).assertDisplayed() + onView(allOf(withId(R.id.submissionStatus), withText(R.string.submitted))).assertDisplayed() + } + + fun verifyAssignmentGraded(score: String) { + onView(allOf(withId(R.id.gradeContainer), isDisplayed())).scrollTo().assertDisplayed() + onView(allOf(withId(R.id.score), isDisplayed())).scrollTo().assertContainsText(score) + onView(allOf(withId(R.id.submissionStatus), withText(R.string.gradedSubmissionLabel))).assertDisplayed() + } + + fun refresh() { + onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayed())).swipeDown() + } + + fun goToSubmissionDetails() { + onView(withId(R.id.submissionAndRubricLabel)).scrollTo().click() + } } + diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt index 8ed2db35af..bba47c4df5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt @@ -16,11 +16,24 @@ */ package com.instructure.student.ui.pages +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withChild +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withParent +import androidx.test.espresso.matcher.ViewMatchers.withText import com.instructure.dataseeding.model.AssignmentApiModel import com.instructure.student.R import com.instructure.espresso.* import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.waitForViewWithText +import org.hamcrest.Matcher +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.containsString class AssignmentListPage : BasePage(pageResId = R.id.assignmentListPage) { @@ -43,8 +56,44 @@ class AssignmentListPage : BasePage(pageResId = R.id.assignmentListPage) { emptyView.assertDisplayed() } - fun assertHasAssignment(assignment: AssignmentApiModel) { + fun assertHasAssignment(assignment: AssignmentApiModel, expectedGrade: String? = null) { + waitForViewWithText(assignment.name).assertDisplayed() + + // Check that either the assignment due date is present, or "No Due Date" is displayed + if(assignment.dueAt != null) { + val matcher = allOf( + withText(containsString("Due: ")), + withId(R.id.date), + withParent(withParent(withChild(withText(assignment.name))))) + scrollToAndAssertDisplayed(matcher) + } + else { + val matcher = allOf( + withText(R.string.toDoNoDueDate), + withId(R.id.date), + withParent(withParent(withChild(withText(assignment.name))))) + scrollToAndAssertDisplayed(matcher) + } + + // Check that grade is present, if that is specified + if(expectedGrade != null) { + val matcher = allOf( + withText(containsString(expectedGrade)), // grade might be "13", total string "13/15" + withId(R.id.points), + withParent(withParent(withChild(withText(assignment.name))))) + scrollToAndAssertDisplayed(matcher) + } + } + + fun refresh() { + onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayed())).swipeDown() + } + + private fun scrollToAndAssertDisplayed(matcher: Matcher) { + onView(allOf(withId(R.id.listView), ViewMatchers.isDisplayed())) + .perform(RecyclerViewActions.scrollTo(ViewMatchers.hasDescendant(matcher))) + .assertDisplayed() } fun assertHasGradingPeriods() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt new file mode 100644 index 0000000000..013081c57d --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt @@ -0,0 +1,31 @@ +package com.instructure.student.ui.pages + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.instructure.espresso.click +import com.instructure.espresso.page.BasePage +import com.instructure.student.R +import org.hamcrest.Matcher +import org.hamcrest.Matchers.allOf + +class CourseBrowserPage : BasePage(R.id.courseBrowserPage) { + + fun selectAssignments() { + val matcher = allOf(withText("Assignments")) + selectSection(matcher) + } + + private fun selectSection(matcher: Matcher) { + // Scroll RecyclerView item into view, if necessary + onView(allOf(withId(R.id.courseBrowserRecyclerView), isDisplayed())) + .perform(RecyclerViewActions.scrollTo(hasDescendant(matcher))) + + onView(matcher).click() + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt index 31e793815e..cee161e74e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt @@ -121,5 +121,10 @@ class DashboardPage : BasePage(R.id.dashboardPage) { Espresso.onView(matcher).assertDisplayed() } + fun selectCourse(course: CourseApiModel) { + assertDisplaysCourse(course) + onView(withText(course.name)).click() + } + } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt index a74f3f0bc6..80bdec00f1 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt @@ -16,8 +16,44 @@ */ package com.instructure.student.ui.pages +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.espresso.OnViewWithStringTextIgnoreCase +import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.student.R +import com.instructure.student.ui.pages.renderPages.SubmissionCommentsRenderPage +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.anyOf +import org.hamcrest.Matchers.containsString open class SubmissionDetailsPage : BasePage(R.id.submissionDetails) { + private val commentsButton by OnViewWithStringTextIgnoreCase("comments") + + private val submissionCommentsRenderPage = SubmissionCommentsRenderPage() + + fun openComments() { + commentsButton.click() + } + + /** + * Assert that a comment is displayed + * [description] contains some text that is in the comment + * [user] is the author of the comment + */ + fun assertCommentDisplayed(description: String, user: CanvasUserApiModel) { + val commentMatcher = allOf( + withId(R.id.commentHolder), + hasDescendant(allOf(withText(user.shortName), withId(R.id.userNameTextView))), + hasDescendant(allOf(withText(containsString(description)), anyOf(withId(R.id.titleTextView), withId(R.id.commentTextView)))) + ) + + submissionCommentsRenderPage.scrollAndAssertDisplayed(commentMatcher) + } + + fun addAndSendComment(comment: String) { + submissionCommentsRenderPage.addAndSendComment(comment) + } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionCommentsRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionCommentsRenderPage.kt index 10d8958200..85fbe9f3ad 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionCommentsRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionCommentsRenderPage.kt @@ -29,7 +29,9 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click +import com.instructure.espresso.closeSoftKeyboard import com.instructure.espresso.page.BasePage +import com.instructure.espresso.typeText import com.instructure.student.R import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.CommentItemState import org.hamcrest.Matcher @@ -73,7 +75,7 @@ class SubmissionCommentsRenderPage: BasePage(R.id.submissionCommentsPage) { } } - private fun scrollAndAssertDisplayed(matcher: Matcher) { + fun scrollAndAssertDisplayed(matcher: Matcher) { onView(allOf(withId(R.id.recyclerView), isDisplayed())) .perform(RecyclerViewActions.scrollTo(ViewMatchers.hasDescendant(matcher))) onView(matcher).assertDisplayed() @@ -91,6 +93,13 @@ class SubmissionCommentsRenderPage: BasePage(R.id.submissionCommentsPage) { fun clickInCommentBox() { commentInput.click() } + + fun addAndSendComment(comment: String) { + clickInCommentBox() + commentInput.typeText(comment) + commentInput.closeSoftKeyboard() + onView(withId(R.id.sendCommentButton)).click() + } } // Custom action to get the left offset of a view and deposit it in the diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index 83631e60dd..7943daa350 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -40,5 +40,7 @@ abstract class StudentTest : CanvasTest() { val loginFindSchoolPage = LoginFindSchoolPage() val loginLandingPage = LoginLandingPage() val loginSignInPage = LoginSignInPage() - + val courseBrowserPage = CourseBrowserPage() + val assignmentDetailsPage = AssignmentDetailsPage() + val submissionDetailsPage = SubmissionDetailsPage() } diff --git a/apps/student/src/main/res/layout/fragment_course_browser.xml b/apps/student/src/main/res/layout/fragment_course_browser.xml index 96338119b4..7fbfa14072 100644 --- a/apps/student/src/main/res/layout/fragment_course_browser.xml +++ b/apps/student/src/main/res/layout/fragment_course_browser.xml @@ -19,7 +19,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/canvasBackgroundWhite" - android:orientation="vertical"> + android:orientation="vertical" + android:id="@+id/courseBrowserPage">