Skip to content
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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can put this seeding before the login as we usually do in our tests. (In this case there will be more time for the API call to propagate).

val testQuizList = seedQuizzes(courseId = course.id, quizzes = 1, withDescription = true, teacherToken = teacher.token, published = true)

Log.d(STEP_TAG, "Refresh the page.")
quizListPage.refresh()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you put the seeding before the login, you don't even need this 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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd call these 'newQuizTitle' and 'newQuizDescription' as these will be the values that the old values will be changed to.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I commented on the methods' impementations, these methods aren'T testing the "change" process, just validating the current value. Please refactor logs accordingly. Also, you should get the 'old' values from the seeded data so you will be able to write logs like:

Log.d(ASSERTION_TAG, "Assert that the quiz name and description have been changed FROM: '$originalQuizTitle' and '$originalQuizDescription' TO: '$newQuizTitle' and '$newQuizDescription'.")

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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can merge the logs of these 2 steps.

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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to put the values to the log as well to see what date and time will be set.

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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value should be in the log to be clear what date will be removed.

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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can merge this and the previous steps logs.

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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to get the element twice, you can just do:

onViewWithContentDescription("due_date_$overrideIndex").scrollTo().click()

onViewWithContentDescription("due_date_$overrideIndex").click()
}
fun clickEditDueTime(overrideIndex: Int = 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing empty line between the functions

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please call this method 'assertQuizDescriptionDisplayed' because it is actually just checking the description, not validating change. If you use this to validate a change process in your test, then write the logs accordingly, but this method itself does not test the change, just the current value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I know 'assertQuizNameChanged' is a legacy method but please refactor that one as well.

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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using the KDoc format in the teacher app, please comment up these functions like the existing ones.

waitForViewWithId(R.id.canvasWebView)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't be sure if the canvasWebView container is loaded that implicates that the preview is loaded successfully.

I'd put some other assertions below it, like calling the assertQuizTitleDisplayed and assertQuizDescriptionDisplayed methods. (And you can use just the assertPreviewDisplayed method in the test, so the other 2 methods may could be private if you don't use them elsewhere).

}

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