Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
*/
Expand All @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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)))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)!!
}
Expand All @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
Loading