Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -534,7 +534,7 @@ class AssignmentsE2ETest: StudentComposeTest() {
@E2E
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.E2E)
@Stub("Failing on CI, needs to be fixed in ticket MBL-18749")
@Stub("Worker issue, failing on CI, needs to be fixed in ticket MBL-18749")
fun testPercentageFileAssignmentWithCommentE2E() {

Log.d(PREPARATION_TAG, "Seeding data.")
Expand Down Expand Up @@ -757,7 +757,7 @@ class AssignmentsE2ETest: StudentComposeTest() {
@E2E
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.COMMENTS, TestCategory.E2E)
@Stub("Failing on CI, needs to be fixed in ticket MBL-18749")
@Stub("Worker issue, failing on CI, needs to be fixed in ticket MBL-18749")
fun testMediaCommentsE2E() {

Log.d(PREPARATION_TAG, "Seeding data.")
Expand Down Expand Up @@ -922,6 +922,78 @@ class AssignmentsE2ETest: StudentComposeTest() {
submissionDetailsPage.assertSelectedAttempt("Attempt 1")
}

@E2E
@Test
@TestMetaData(Priority.COMMON, FeatureCategory.ASSIGNMENTS, TestCategory.E2E)
fun test1DraftAssignmentE2E() {

Log.d(PREPARATION_TAG, "Seeding data.")
val data = seedData(teachers = 1, courses = 1, students = 1)
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val course = data.coursesList[0]

Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.")
val pointsTextAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))

Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
dashboardPage.waitForRender()

Log.d(STEP_TAG, "Select course: '${course.name}'.")
dashboardPage.selectCourse(course)

Log.d(STEP_TAG, "Navigate to course Assignments Page.")
courseBrowserPage.selectAssignments()

Log.d(ASSERTION_TAG, "Assert that our assignments are present," +
"along with any grade/date info.")
assignmentListPage.assertHasAssignment(pointsTextAssignment)

Log.d(STEP_TAG, "Click on assignment '${pointsTextAssignment.name}'.")
assignmentListPage.clickAssignment(pointsTextAssignment)

Log.d(ASSERTION_TAG, "Assert that 'Submission & Rubric' label is displayed and navigate to Submission Details Page.")
assignmentDetailsPage.assertSubmissionAndRubricLabel()

Log.d(STEP_TAG, "Click on the 'Submit Assignment' button.")
assignmentDetailsPage.clickSubmit()

Log.d(STEP_TAG," Type some text into the submission text input and click on the back button to trigger the 'Save Draft' dialog, then click on the 'Don't Save' button on the 'Save Draft' pop-up dialog to not save the draft submission.")
val draftText = "Draft submission text"
textSubmissionUploadPage.typeText(draftText)
textSubmissionUploadPage.clickToolbarBackButton()
textSubmissionUploadPage.clickDontSaveDraft()

Log.d(ASSERTION_TAG, "Assert that 'Submission & Rubric' label is displayed and navigate to Submission Details Page.")
assignmentDetailsPage.assertSubmissionAndRubricLabel()

Log.d(STEP_TAG, "Click on the 'Submit Assignment' button.")
assignmentDetailsPage.clickSubmit()

Log.d(STEP_TAG," Type some text into the submission text input and click on the back button to trigger the 'Save Draft' dialog, then click on the 'Save' button on the 'Save Draft' pop-up dialog to make a draft submission.")
textSubmissionUploadPage.typeText(draftText)
textSubmissionUploadPage.clickToolbarBackButton()
textSubmissionUploadPage.clickSaveDraft()

Log.d(ASSERTION_TAG, "Assert that the Draft submission info (title, subtitle) are displayed on the Assignment Details Page since we saved a draft.")
assignmentDetailsPage.assertDraftAvailableInformation()

Log.d(STEP_TAG, "Click on the 'Draft Available' link to open the saved draft assignment.")
assignmentDetailsPage.clickDraftSubmission()

Log.d(ASSERTION_TAG, "Assert that the previously saved text ($draftText) is displayed in the text submission input.")
textSubmissionUploadPage.assertTextSubmissionDisplayed(draftText)

Log.d(STEP_TAG, "Click on the 'Submit' button to submit the draft assignment.")
textSubmissionUploadPage.clickOnSubmitButton()
handleWorkManagerTask("SubmissionWorker")

Log.d(ASSERTION_TAG, "Assert that the assignment's status is submitted and the 'Successfully submitted!' label is displayed.")
assignmentDetailsPage.assertStatusSubmitted()
assignmentDetailsPage.assertAssignmentSubmitted()
}

@E2E
@Test
@TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.E2E)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.instructure.student.ui.pages.compose

import androidx.compose.ui.test.junit4.ComposeTestRule
import com.instructure.canvas.espresso.TypeInRCETextEditor
import com.instructure.canvas.espresso.explicitClick
import com.instructure.composetest.clickToolbarIconButton
import com.instructure.espresso.OnViewWithId
import com.instructure.espresso.OnViewWithText
import com.instructure.espresso.RCETextEditorContentAssertion
import com.instructure.espresso.RCETextEditorHtmlAssertion
import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
import com.instructure.espresso.page.onView
import com.instructure.espresso.page.waitForViewWithId
import com.instructure.espresso.page.waitForViewWithText
import com.instructure.espresso.page.withText
import com.instructure.student.R

class TextSubmissionUploadPage(private val composeTestRule: ComposeTestRule) : BasePage(R.id.textSubmissionUpload) {

val submitButton by OnViewWithId(R.id.menuSubmit)
val contentRceView by OnViewWithId(R.id.rce_webView)
val textEntryLabel by OnViewWithText(R.string.textEntry)

fun typeText(textToType: String) {
contentRceView.perform(TypeInRCETextEditor(textToType))
}

fun clickToolbarBackButton() {
composeTestRule.clickToolbarIconButton("Back")
}

fun clickOnSubmitButton() {
submitButton.perform(explicitClick())
}

fun clickCancel() {
onView(withText(com.instructure.pandautils.R.string.cancel)).click()
}

fun clickSaveDraft() {
waitForViewWithText(com.instructure.pandautils.R.string.save).click()
}

fun clickDontSaveDraft() {
waitForViewWithText(com.instructure.pandautils.R.string.dontSave).click()
}

fun assertTextSubmissionContentDescriptionDisplayed(expectedText: String) {
waitForViewWithId(com.instructure.pandautils.R.id.rce_webView).check(
RCETextEditorContentAssertion(expectedText)
)
}

fun assertTextSubmissionDisplayed(expectedHtml: String) {
waitForViewWithId(com.instructure.pandautils.R.id.rce_webView).check(
RCETextEditorHtmlAssertion(expectedHtml)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import com.instructure.espresso.ModuleItemInteractions
import com.instructure.student.R
import com.instructure.student.activity.LoginActivity
import com.instructure.student.ui.pages.classic.StudentAssignmentDetailsPage
import com.instructure.student.ui.pages.compose.TextSubmissionUploadPage
import org.junit.Rule

abstract class StudentComposeTest : StudentTest() {
Expand All @@ -67,5 +68,12 @@ abstract class StudentComposeTest : StudentTest() {
val inboxSignatureSettingsPage = InboxSignatureSettingsPage(composeTestRule)
val toDoListPage = ToDoListPage(composeTestRule)
val toDoFilterPage = ToDoFilterPage(composeTestRule)
val assignmentDetailsPage = StudentAssignmentDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item), composeTestRule)
val assignmentDetailsPage = StudentAssignmentDetailsPage(
ModuleItemInteractions(
R.id.moduleName,
R.id.next_item,
R.id.prev_item
), composeTestRule
)
val textSubmissionUploadPage = TextSubmissionUploadPage(composeTestRule)
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ import com.instructure.student.ui.pages.classic.ShareExtensionStatusPage
import com.instructure.student.ui.pages.classic.ShareExtensionTargetPage
import com.instructure.student.ui.pages.classic.SubmissionDetailsPage
import com.instructure.student.ui.pages.classic.SyllabusPage
import com.instructure.student.ui.pages.classic.TextSubmissionUploadPage
import com.instructure.student.ui.pages.classic.UrlSubmissionUploadPage
import com.instructure.student.ui.pages.classic.k5.ElementaryCoursePage
import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage
Expand Down Expand Up @@ -161,7 +160,6 @@ abstract class StudentTest : CanvasTest() {
val pushNotificationsPage = PushNotificationsPage()
val emailNotificationsPage = EmailNotificationsPage()
val submissionDetailsPage = SubmissionDetailsPage()
val textSubmissionUploadPage = TextSubmissionUploadPage()
val syllabusPage = SyllabusPage()
val urlSubmissionUploadPage = UrlSubmissionUploadPage()
val elementaryDashboardPage = ElementaryDashboardPage()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ class AssignmentE2ETest : TeacherComposeTest() {
@E2E
@Test
@TestMetaData(Priority.COMMON, FeatureCategory.COMMENTS, TestCategory.E2E)
@Stub("Failing on CI, needs to be fixed in ticket MBL-18749")
@Stub("Worker issue, failing on CI, needs to be fixed in ticket MBL-18749")
fun testMediaCommentsE2E() {

Log.d(PREPARATION_TAG, "Seeding data.")
Expand Down
1 change: 1 addition & 0 deletions automation/espresso/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ dependencies {
androidTestImplementation Libs.COMPOSE_UI_TEST

implementation project(':pandautils')
implementation project(':rceditor')

// last update: Sept 30 2017
// old versions: $ANDROID_HOME/extras/android/m2repository/com/android/support/test/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import androidx.viewpager.widget.ViewPager
import com.instructure.espresso.assertDisplayed
import com.instructure.espresso.retryWithIncreasingDelay
import com.instructure.espresso.swipeUp
import instructure.rceditor.RCETextEditor
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.hamcrest.Matchers.allOf
Expand Down Expand Up @@ -381,4 +382,21 @@ fun waitForViewToDisappear(viewMatcher: Matcher<View>, timeoutInSeconds: Long) {

fun toString(view: View): String {
return HumanReadables.getViewHierarchyErrorMessage(view, null, "", null)
}

class TypeInRCETextEditor(val text: String) : ViewAction {
override fun getDescription(): String {
return "Enters text into an RCETextEditor"
}

override fun getConstraints(): Matcher<View> {
return ViewMatchers.isAssignableFrom(RCETextEditor::class.java)
}

override fun perform(uiController: UiController?, view: View?) {
when(view) {
is RCETextEditor -> view.applyHtml(text)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti
onView(withText(R.string.done)).click()
}

fun clickDraftSubmission() {
onView(withId(R.id.draftTitle) + withText(R.string.submissionDraftAvailableTitle)).click()
}

fun clickSubmissionAndRubric() {
onView(allOf(withId(R.id.submissionAndRubricLabel), withText(R.string.submissionAndRubric))).click()
}
Expand All @@ -320,6 +324,12 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti
onView(withContentDescription("Send a message about this assignment")).click()
}

fun assertDraftAvailableInformation() {
onView(withId(R.id.draftTitle) + withText(R.string.submissionDraftAvailableTitle)).assertDisplayed()
onView(withId(R.id.draftSubtitle) + withText(R.string.submissionDraftAvailableSubtitle)).assertDisplayed()
onView(withId(R.id.draftDivider)).assertDisplayed()
}

//OfflineMethod
fun assertSubmitButtonDisabled() {
onView(withId(R.id.submitButton)).check(matches(ViewMatchers.isNotEnabled()))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2026 - present Instructure, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.instructure.composetest

import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.performClick

/**
* Clicks an IconButton within a TopAppBar by content description.
*
* @param contentDescription The content description of the IconButton to click (e.g., "Back", "More options")
* @param toolbarTag The test tag of the TopAppBar, defaults to "toolbar"
*/
fun ComposeTestRule.clickToolbarIconButton(
contentDescription: String,
toolbarTag: String = "toolbar"
) {
waitForIdle()
onNode(
hasParent(hasTestTag(toolbarTag)).and(
hasContentDescription(contentDescription)
)
).performClick()
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.viewpager.widget.ViewPager
import com.google.android.material.bottomnavigation.BottomNavigationView
import instructure.rceditor.RCETextEditor
import junit.framework.AssertionFailedError
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue

class RecyclerViewItemCountAssertion(private val expectedCount: Int) : ViewAssertion {
override fun check(view: View, noViewFoundException: NoMatchingViewException?) {
Expand Down Expand Up @@ -128,3 +130,29 @@ class ViewAlphaAssertion(private val expectedAlpha: Float): ViewAssertion {
assertThat("View alpha should be $expectedAlpha", view.alpha, `is`(expectedAlpha))
}
}

class RCETextEditorContentAssertion(private val expectedText: String) : ViewAssertion {
override fun check(view: View, noViewFoundException: NoMatchingViewException?) {
noViewFoundException?.let { throw it }
val rceEditor = (view as? RCETextEditor)
?: throw ClassCastException("View of type ${view.javaClass.simpleName} must be an RCETextEditor")
val actualContent = rceEditor.accessibilityContentDescription
assertTrue(
"Expected RCE content to contain '$expectedText', but was '$actualContent'",
actualContent.contains(expectedText)
)
}
}

class RCETextEditorHtmlAssertion(private val expectedHtml: String) : ViewAssertion {
override fun check(view: View, noViewFoundException: NoMatchingViewException?) {
noViewFoundException?.let { throw it }
val rceEditor = (view as? RCETextEditor)
?: throw ClassCastException("View of type ${view.javaClass.simpleName} must be an RCETextEditor")
val actualHtml = rceEditor.getHtml() ?: ""
assertTrue(
"Expected RCE HTML to contain '$expectedHtml', but was '$actualHtml'",
actualHtml.contains(expectedHtml)
)
}
}
Loading