diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/QuizE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/QuizE2ETest.kt index 7955ef3364..bca5f1b2a8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/QuizE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/QuizE2ETest.kt @@ -23,6 +23,7 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 @@ -143,4 +144,127 @@ class QuizE2ETest: TeacherTest() { quizListPage.assertHasQuiz(secondQuiz.title) } + @E2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.QUIZZES, TestCategory.E2E) + fun testQuizEditAndPreviewE2E() { + + Log.d(PREPARATION_TAG, "Seeding 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] + + Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.") + tokenLogin(teacher) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open '${course.name}' course and navigate to Quizzes Page.") + dashboardPage.openCourse(course.name) + courseBrowserPage.openQuizzesTab() + + Log.d(PREPARATION_TAG, "Seed a quiz for the '${course.name}' course.") + val testQuizList = seedQuizzes(courseId = course.id, quizzes = 1, withDescription = true, teacherToken = teacher.token, published = true) + + Log.d(STEP_TAG, "Refresh the page.") + quizListPage.refresh() + + val quiz = testQuizList.quizList[0] + Log.d(STEP_TAG, "Click on the quiz: '${quiz.title}'.") + quizListPage.clickQuiz(quiz.title) + + val quizTitle = "My Custom Quiz Title" + val quizDescription = "This is my custom quiz description" + Log.d(STEP_TAG, "Open 'Edit' page and edit the quiz description to: '$quizDescription' and title to: '$quizTitle'.") + quizDetailsPage.openEditPage() + editQuizDetailsPage.editQuizDescription(quizDescription) + editQuizDetailsPage.editQuizTitle(quizTitle) + + Log.d(ASSERTION_TAG, "Assert that the quiz name and description have been changed to: '$quizTitle' and '$quizDescription'.") + quizDetailsPage.assertQuizNameChanged(quizTitle) + quizDetailsPage.assertQuizDescriptionChanged(quizDescription) + + Log.d(STEP_TAG, "Open preview page.") + quizDetailsPage.openPreviewPage() + + Log.d(ASSERTION_TAG, "Assert that the preview loaded and displays the edited quiz title: '$quizTitle' and the edited quiz description: '$quizDescription'.") + quizPreviewPage.assertPreviewLoaded() + quizPreviewPage.assertQuizTitleDisplayed(quizTitle) + quizPreviewPage.assertQuizDescriptionDisplayed(quizDescription) + + Log.d(STEP_TAG, "Go back to Quiz Details page.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Open Due Dates section.") + quizDetailsPage.openAllDatesPage() + + Log.d(STEP_TAG, "Click the pencil/edit icon to open the edit page.") + assignmentDueDatesPage.openEditPage() + + Log.d(STEP_TAG, "Set due date and time for the first due date.") + editQuizDetailsPage.clickEditDueDate() + editQuizDetailsPage.editDate(2025, 5, 15) + editQuizDetailsPage.clickEditDueTime() + editQuizDetailsPage.editTime(10, 30) + + Log.d(STEP_TAG, "Click 'Add Override' to add a second due date and assign it to '${student.name}'.") + editQuizDetailsPage.clickAddOverride() + assigneeListPage.toggleAssignees(listOf(student.name)) + assigneeListPage.saveAndClose() + + Log.d(ASSERTION_TAG, "Assert that another new due date override has been created.") + editQuizDetailsPage.assertNewOverrideCreated() + + Log.d(STEP_TAG, "Set due date and time for the second override.") + editQuizDetailsPage.clickEditDueDate(1) + editQuizDetailsPage.editDate(2025, 6, 20) + editQuizDetailsPage.clickEditDueTime(1) + editQuizDetailsPage.editTime(14, 45) + + Log.d(STEP_TAG, "Save the quiz after creating 2 due dates.") + editQuizDetailsPage.saveQuiz() + + Log.d(STEP_TAG, "Refresh the Due Dates page.") + refresh() + + Log.d(ASSERTION_TAG, "Assert that 2 due dates are visible on the Due Dates page.") + assignmentDueDatesPage.assertDueDatesCount(2) + + Log.d(ASSERTION_TAG, "Assert first due date is for 'Everyone else' with date 'May 15, 2025 at 10:30 AM'.") + assignmentDueDatesPage.assertDueFor("Everyone else") + assignmentDueDatesPage.assertDueDateTime("May 15, 2025 at 10:30 AM") + + Log.d(ASSERTION_TAG, "Assert second due date is for '${student.name}' with date 'Jun 20, 2025 at 2:45 PM'.") + assignmentDueDatesPage.assertDueFor(student.name) + assignmentDueDatesPage.assertDueDateTime("Jun 20, 2025 at 2:45 PM") + + Log.d(STEP_TAG, "Press back to return to Quiz Details page.") + Espresso.pressBack() + + Log.d(ASSERTION_TAG, "Assert that the due dates section shows 'Multiple Due Dates'.") + quizDetailsPage.assertMultipleDueDatesTextDisplayed() + + Log.d(STEP_TAG, "Open Due Dates section again.") + quizDetailsPage.openAllDatesPage() + + Log.d(STEP_TAG, "Click the pencil/edit icon to open the edit page.") + assignmentDueDatesPage.openEditPage() + + Log.d(STEP_TAG, "Remove the second due date.") + editQuizDetailsPage.removeSecondOverride() + + Log.d(STEP_TAG, "Save the quiz after removing the second due date.") + editQuizDetailsPage.saveQuiz() + + Log.d(STEP_TAG, "Refresh the Due Dates page.") + refresh() + + Log.d(ASSERTION_TAG, "Assert that only 1 due date is visible on the Due Dates page.") + assignmentDueDatesPage.assertDisplaysSingleDueDate() + + Log.d(ASSERTION_TAG, "Assert remaining due date is for 'Everyone' with date 'May 15, 2025 at 10:30 AM'.") + assignmentDueDatesPage.assertDueFor("Everyone") + assignmentDueDatesPage.assertDueDateTime("May 15, 2025 at 10:30 AM") + } + } \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditQuizDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditQuizDetailsPage.kt index 9db7b2eb49..e525d192c3 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditQuizDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditQuizDetailsPage.kt @@ -50,6 +50,7 @@ import com.instructure.espresso.randomString import com.instructure.espresso.replaceText import com.instructure.espresso.scrollTo import com.instructure.teacher.R +import com.instructure.teacher.ui.utils.TypeInRCETextEditor import com.instructure.teacher.view.AssignmentOverrideView import org.hamcrest.CoreMatchers.allOf import org.hamcrest.Matchers @@ -69,6 +70,7 @@ class EditQuizDetailsPage : BasePage() { private val accessCodeEditText by WaitForViewWithId(R.id.editAccessCode) private val saveButton by OnViewWithId(R.id.menuSave) private val descriptionWebView by OnViewWithId(R.id.descriptionWebView, autoAssert = false) + private val contentRceView by WaitForViewWithId(R.id.rce_webView, autoAssert = false) private val noDescriptionTextView by OnViewWithId( R.id.noDescriptionTextView, autoAssert = false @@ -91,6 +93,15 @@ class EditQuizDetailsPage : BasePage() { saveQuiz() } + /** + * Edits the quiz description with the specified new description. + * + * @param newDescription The new description to be set as the quiz description. + */ + fun editQuizDescription(newDescription: String) { + contentRceView.perform(TypeInRCETextEditor(newDescription)) + } + /** * Clicks on the access code switch to toggle its state. */ @@ -258,8 +269,18 @@ class EditQuizDetailsPage : BasePage() { ) fun editAssignees() = waitScrollClick(R.id.assignTo) - fun clickEditDueDate() = waitScrollClick(R.id.dueDate) - fun clickEditDueTime() = waitScrollClick(R.id.dueTime) + fun clickEditDueDate(overrideIndex: Int = 0) { + addOverrideButton().scrollTo() + Thread.sleep(1000) //wait for the UI to be settled + onViewWithContentDescription("due_date_$overrideIndex").scrollTo() + onViewWithContentDescription("due_date_$overrideIndex").click() + } + fun clickEditDueTime(overrideIndex: Int = 0) { + addOverrideButton().scrollTo() + Thread.sleep(1000) //wait for the UI to be settled + onViewWithContentDescription("due_time_$overrideIndex").scrollTo() + onViewWithContentDescription("due_time_$overrideIndex").click() + } fun clickEditUnlockDate() = waitScrollClick(R.id.fromDate) fun clickEditUnlockTime() = waitScrollClick(R.id.fromTime) fun clickEditLockDate() = waitScrollClick(R.id.toDate) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizDetailsPage.kt index 0deec368ca..0248f212c5 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizDetailsPage.kt @@ -16,6 +16,11 @@ package com.instructure.teacher.ui.pages.classic import androidx.test.InstrumentationRegistry +import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches +import androidx.test.espresso.web.sugar.Web.onWebView +import androidx.test.espresso.web.webdriver.DriverAtoms.findElement +import androidx.test.espresso.web.webdriver.DriverAtoms.getText +import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvasapi2.models.Quiz import com.instructure.dataseeding.model.QuizApiModel import com.instructure.espresso.ModuleItemInteractions @@ -33,11 +38,14 @@ import com.instructure.espresso.assertVisible import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView +import com.instructure.espresso.page.plus import com.instructure.espresso.page.scrollTo import com.instructure.espresso.page.waitForView import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withText import com.instructure.espresso.swipeDown import com.instructure.teacher.R +import org.hamcrest.Matchers.containsString /** * Represents the Quiz Details page. @@ -71,6 +79,7 @@ class QuizDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : Base private val gradedDonut by OnViewWithId(R.id.gradedWrapper) private val ungradedDonut by OnViewWithId(R.id.ungradedWrapper) private val notSubmittedDonut by OnViewWithId(R.id.notSubmittedWrapper) + private val quizPreviewButton by OnViewWithId(R.id.quizPreviewButton) /** * Asserts that the instructions for the quiz are displayed. @@ -91,9 +100,18 @@ class QuizDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : Base * Opens the All Dates page for the quiz. */ fun openAllDatesPage() { + scrollTo(R.id.dueLayout) dueDatesLayout.click() } + /** + * Asserts that the due dates section displays "Multiple Due Dates". + */ + fun assertMultipleDueDatesTextDisplayed() { + dueSectionLabel.assertDisplayed() + onView(withId(R.id.otherDueDateTextView) + withText(R.string.multiple_due_dates)).assertDisplayed() + } + /** * Opens the Edit page for the quiz. */ @@ -109,6 +127,14 @@ class QuizDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : Base viewAllSubmissions.click() } + /** + * Opens the Preview page for the quiz. + */ + fun openPreviewPage() { + scrollTo(R.id.quizPreviewButton) + quizPreviewButton.click() + } + /** * Asserts the quiz details such as title and publish status. * @@ -177,6 +203,19 @@ class QuizDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : Base quizTitleTextView.assertHasText(newQuizName) } + /** + * Asserts that the quiz description has changed to the specified new description. + * + * @param newDescription The new description to assert. + */ + fun assertQuizDescriptionChanged(newDescription: String) { + scrollTo(R.id.contentWebView) + instructionsWebView.assertVisible() + onWebView() + .withElement(findElement(Locator.TAG_NAME, "body")) + .check(webMatches(getText(), containsString(newDescription))) + } + /** * Asserts that the quiz points have changed to the specified new quiz points. * diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizPreviewPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizPreviewPage.kt new file mode 100644 index 0000000000..a4af4c7295 --- /dev/null +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizPreviewPage.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * 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 + * distributed 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 the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.teacher.ui.pages.classic + +import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches +import androidx.test.espresso.web.sugar.Web.onWebView +import androidx.test.espresso.web.webdriver.DriverAtoms.findElement +import androidx.test.espresso.web.webdriver.DriverAtoms.getText +import androidx.test.espresso.web.webdriver.Locator +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.waitForViewWithId +import com.instructure.teacher.R +import org.hamcrest.Matchers.containsString + +class QuizPreviewPage : BasePage(R.id.canvasWebView) { + + fun assertPreviewLoaded() { + waitForViewWithId(R.id.canvasWebView) + } + + fun assertQuizTitleDisplayed(quizTitle: String) { + onWebView() + .withElement(findElement(Locator.TAG_NAME, "body")) + .check(webMatches(getText(), containsString(quizTitle))) + } + + fun assertQuizDescriptionDisplayed(description: String) { + onWebView() + .withElement(findElement(Locator.TAG_NAME, "body")) + .check(webMatches(getText(), containsString(description))) + } + +} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt index b28c3be183..6a13073f82 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt @@ -72,6 +72,7 @@ import com.instructure.teacher.ui.pages.classic.ProfileSettingsPage import com.instructure.teacher.ui.pages.classic.PushNotificationsPage import com.instructure.teacher.ui.pages.classic.QuizDetailsPage import com.instructure.teacher.ui.pages.classic.QuizListPage +import com.instructure.teacher.ui.pages.classic.QuizPreviewPage import com.instructure.teacher.ui.pages.classic.RemoteConfigSettingsPage import com.instructure.teacher.ui.pages.classic.SpeedGraderCommentsPage import com.instructure.teacher.ui.pages.classic.SpeedGraderQuizSubmissionPage @@ -136,6 +137,7 @@ abstract class TeacherTest : CanvasTest() { val peopleListPage = PeopleListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn)) val quizDetailsPage = QuizDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next, R.id.previous)) val quizListPage = QuizListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn, R.id.backButton)) + val quizPreviewPage = QuizPreviewPage() val speedGraderCommentsPage = SpeedGraderCommentsPage() val speedGraderQuizSubmissionPage = SpeedGraderQuizSubmissionPage() val personContextPage = PersonContextPage() diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizPreviewWebviewFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizPreviewWebviewFragment.kt index 62e77813c3..af9493b4df 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizPreviewWebviewFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizPreviewWebviewFragment.kt @@ -107,6 +107,7 @@ class QuizPreviewWebviewFragment : InternalWebViewFragment() { @JvmStatic val TITLE = "title" fun newInstance(args: Bundle) = QuizPreviewWebviewFragment().apply { + arguments = args url = args.getString(URL)!! title = args.getString(TITLE)!! } @@ -116,6 +117,7 @@ class QuizPreviewWebviewFragment : InternalWebViewFragment() { args.putString(URL, url) args.putString(TITLE, title) args.putBoolean(DARK_TOOLBAR, false) + args.putBoolean(AUTHENTICATE, true) return args } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/view/AssignmentOverrideView.kt b/apps/teacher/src/main/java/com/instructure/teacher/view/AssignmentOverrideView.kt index 87c699f1e4..5ddef2196b 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/view/AssignmentOverrideView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/view/AssignmentOverrideView.kt @@ -30,7 +30,8 @@ import com.instructure.teacher.R import com.instructure.teacher.databinding.ViewAssignmentOverrideBinding import com.instructure.teacher.models.DueDateGroup import com.instructure.teacher.utils.formatOrDoubleDash -import java.util.* +import java.util.Calendar +import java.util.Date import kotlin.properties.Delegates class AssignmentOverrideView @JvmOverloads constructor( @@ -194,7 +195,11 @@ class AssignmentOverrideView @JvmOverloads constructor( if (!showRemove) removeOverride.setGone() - if (BuildConfig.IS_TESTING) removeOverride.contentDescription = "remove_override_button_$index" + if (BuildConfig.IS_TESTING) { + removeOverride.contentDescription = "remove_override_button_$index" + binding.dueDateTextInput.contentDescription = "due_date_$index" + binding.dueTimeTextInput.contentDescription = "due_time_$index" + } removeOverride.setOnClickListener { removeOverrideClickListener(dueDateGroup)