Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link

Choose a reason for hiding this comment

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

This change fixes a bug where the fragment's arguments weren't being set, which could cause issues when the fragment is recreated. However, this is a production code fix that should be called out explicitly in the PR description and potentially covered with a unit test.

Consider adding a test to verify that the arguments are properly set when creating a new instance of this fragment.

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

Choose a reason for hiding this comment

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

Adding AUTHENTICATE = true changes the authentication behavior for quiz previews. This is another production code change that should be:

  1. Explicitly documented in the PR description
  2. Verified that it doesn't break existing functionality
  3. Potentially covered with a test to ensure authenticated previews work as expected

Was this missing authentication causing the preview to fail in the E2E test?

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