diff --git a/.github/workflows/open-source-build-parent.yml b/.github/workflows/open-source-build-parent.yml new file mode 100644 index 0000000000..603d999565 --- /dev/null +++ b/.github/workflows/open-source-build-parent.yml @@ -0,0 +1,45 @@ +name: "Open Source Build: Parent" + +on: + schedule: + - cron: '0 9 * * 1' + workflow_dispatch: + +jobs: + build: + name: Build Parent + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run open source script + run: ./open_source.sh + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build DevDebug + run: | + ./gradle/gradlew -p apps :parent:assembleDevDebug \ + --build-cache \ + --parallel \ + --max-workers=4 \ + --no-daemon \ + -Dorg.gradle.jvmargs="-Xmx6g -XX:+HeapDumpOnOutOfMemoryError" + diff --git a/.github/workflows/open-source-build-student.yml b/.github/workflows/open-source-build-student.yml new file mode 100644 index 0000000000..6bab8028e3 --- /dev/null +++ b/.github/workflows/open-source-build-student.yml @@ -0,0 +1,45 @@ +name: "Open Source Build: Student" + +on: + schedule: + - cron: '0 9 * * 1' + workflow_dispatch: + +jobs: + build: + name: Build Student + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run open source script + run: ./open_source.sh + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build DevDebug + run: | + ./gradle/gradlew -p apps :student:assembleDevDebug \ + --build-cache \ + --parallel \ + --max-workers=4 \ + --no-daemon \ + -Dorg.gradle.jvmargs="-Xmx6g -XX:+HeapDumpOnOutOfMemoryError" + diff --git a/.github/workflows/open-source-build-teacher.yml b/.github/workflows/open-source-build-teacher.yml new file mode 100644 index 0000000000..5c49c643c7 --- /dev/null +++ b/.github/workflows/open-source-build-teacher.yml @@ -0,0 +1,45 @@ +name: "Open Source Build: Teacher" + +on: + schedule: + - cron: '0 9 * * 1' + workflow_dispatch: + +jobs: + build: + name: Build Teacher + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run open source script + run: ./open_source.sh + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build DevDebug + run: | + ./gradle/gradlew -p apps :teacher:assembleDevDebug \ + --build-cache \ + --parallel \ + --max-workers=4 \ + --no-daemon \ + -Dorg.gradle.jvmargs="-Xmx6g -XX:+HeapDumpOnOutOfMemoryError" + diff --git a/.github/workflows/release-student.yml b/.github/workflows/release-student.yml index bb2039023b..85462aade5 100644 --- a/.github/workflows/release-student.yml +++ b/.github/workflows/release-student.yml @@ -16,7 +16,7 @@ jobs: with: app-name: student app-display-name: Student - include-e2e-offline: true + include-e2e-offline: false slack-emoji: ':student-app:' secrets: GH_APP_ID: ${{ secrets.GH_APP_ID }} diff --git a/.github/workflows/reusable-release-pipeline.yml b/.github/workflows/reusable-release-pipeline.yml index 3bb7772de3..83a7675028 100644 --- a/.github/workflows/reusable-release-pipeline.yml +++ b/.github/workflows/reusable-release-pipeline.yml @@ -158,13 +158,13 @@ jobs: matrix: include: - test-type: portrait - flank-config: flank.yml + flank-config: flank_portrait_interaction_release.yml - test-type: landscape - flank-config: flank_landscape.yml + flank-config: flank_landscape_interaction_release.yml - test-type: tablet - flank-config: flank_tablet.yml + flank-config: flank_tablet_interaction_release.yml - test-type: e2e - flank-config: flank_e2e.yml + flank-config: flank_e2e_release.yml steps: - name: Checkout repository uses: actions/checkout@v4 @@ -186,7 +186,7 @@ jobs: chmod 600 service-account-key.json - name: Setup Flank config - run: cp ./apps/${{ inputs.app-name }}/${{ matrix.flank-config }} ./flank.yml + run: cp ./apps/${{ inputs.app-name }}/release_tests/${{ matrix.flank-config }} ./flank.yml - name: Copy APKs to expected locations run: | diff --git a/README.md b/README.md index 5bfce5e8d6..6aca1554eb 100644 --- a/README.md +++ b/README.md @@ -6,31 +6,22 @@ The open source code provided by the Android Team at Instructure. ## Building -First, install the Flutter SDK using the instructions found [here](https://flutter.dev/docs/get-started/install). - -Next, run `./open_source.sh` once. You may now use Gradle to build the apps. +Run `./open_source.sh` once. You may now use Gradle to build the apps. ### Student, Teacher and native Parent 1. Open `apps/build.gradle` in Android Studio -``` -Android Studio > Import Project > canvas-android/apps/build.gradle -``` - -2. Select the app from the list of configurations (`student` or `teacher`) -3. Tap 'Run' (`^R`) to run the app - -### Flutter Parent - -1. Open `canvas-android/apps/flutter_parent` in Android Studio. -2. Make sure the `main.dart` configuration is selected + ``` + Android Studio > Import Project > canvas-android/apps/build.gradle + ``` +2. Select the app from the list of configurations 3. Tap 'Run' (`^R`) to run the app -App | Command | Build Status ---- | --- | --- -Student | `./gradle/gradlew -p apps :student:assembleDevDebug` | [![Student build Status](https://app.bitrise.io/app/9417c28328c02b7c/status.svg?token=D7fHdeUlz19PurcEPIQNzw&branch=master)](https://app.bitrise.io/app/9417c28328c02b7c) -Teacher | `./gradle/gradlew -p apps :teacher:assembleDevDebug` | [![Teacher build Status](https://app.bitrise.io/app/4f5339d0ec3436ca/status.svg?token=ATqaYNnYyS4eDUxc0d9EZQ&branch=master)](https://app.bitrise.io/app/4f5339d0ec3436ca) -Parent | (in apps/flutter_parent) `flutter pub get; flutter build apk` | [![Parent build Status](https://app.bitrise.io/app/39fd3312f33be200/status.svg?token=jiiPeSZlSxrx5lkqccLN1Q&branch=master)](https://app.bitrise.io/app/39fd3312f33be200) +| App | Command | Build Status | +|---------|------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Student | `./gradle/gradlew -p apps :student:assembleDevDebug` | [![Student](https://github.com/instructure/canvas-android/actions/workflows/open-source-build-student.yml/badge.svg)](https://github.com/instructure/canvas-android/actions/workflows/open-source-build-student.yml) | +| Teacher | `./gradle/gradlew -p apps :teacher:assembleDevDebug` | [![Teacher](https://github.com/instructure/canvas-android/actions/workflows/open-source-build-teacher.yml/badge.svg)](https://github.com/instructure/canvas-android/actions/workflows/open-source-build-teacher.yml) | +| Parent | `./gradle/gradlew -p apps :parent:assembleDevDebug` | [![Parent](https://github.com/instructure/canvas-android/actions/workflows/open-source-build-parent.yml/badge.svg)](https://github.com/instructure/canvas-android/actions/workflows/open-source-build-parent.yml) | ## Running tests @@ -42,11 +33,11 @@ To run unit tests for Student, Teacher and native Parent #### The Applications we have published on Google Play. -App | Description ---- | --- -[Canvas Student][canvas] | Used by Students all over the world to be smarter, go faster, and do more. -[Canvas Teacher][teacher] | User by Teachers all over the world to update course content or grade on the go. -[Canvas Parent][parent] | Used by Parents all over the world to be parents. +| App | Description | +|---------------------------|----------------------------------------------------------------------------------| +| [Canvas Student][canvas] | Used by Students all over the world to be smarter, go faster, and do more. | +| [Canvas Teacher][teacher] | User by Teachers all over the world to update course content or grade on the go. | +| [Canvas Parent][parent] | Used by Parents all over the world to be parents. | [canvas]: https://play.google.com/store/apps/details?id=com.instructure.candroid [teacher]: https://play.google.com/store/apps/details?id=com.instructure.teacher @@ -56,20 +47,20 @@ App | Description #### These are things that we use internally to create our applications. -Module | Description - --- | --- -annotations | A wrapper for the PSPDFKit library and logic for annotation handling and converting in PDF documents. -buildSrc | Library for common gradle dependencies and gradle transformers that are used by the project. -canvas-api-2 | Canvas for Android Api used to talk to the Canvas LMS and is testable. -dataseedingapi | gRPC wrapper for Canvas that enables creating data to test the apps. -DocumentScanner | A wrapper for document scanning features. -espresso | The UI testing library built on Espresso. -interactions | Interactions for navigation used in the apps. -login-api-2 | The libarary used to make logging in and getting a token relative easy and is testable. -pandares | Collection of resources used in our apps. -pandautils | The core library for the apps. All the common code is implemented here that is reused by the 3 apps. -rceditor | A wrapper for rich content editing used in our apps. -recyclerview | A fancy RecyclerView library that supports expanding and collapsing, pagination, and stuff like that. (deprecated) +| Module | Description | +|----------------|--------------------------------------------------------------------------------------------------------------------| +| annotations | A wrapper for the PSPDFKit library and logic for annotation handling and converting in PDF documents. | +| buildSrc | Library for common gradle dependencies and gradle transformers that are used by the project. | +| canvas-api-2 | Canvas for Android Api used to talk to the Canvas LMS and is testable. | +| dataseedingapi | gRPC wrapper for Canvas that enables creating data to test the apps. | +| espresso | The UI testing library built on Espresso. | +| horizon | Canvas Career experience for the Student app. | +| interactions | Interactions for navigation used in the apps. | +| login-api-2 | The libarary used to make logging in and getting a token relative easy and is testable. | +| pandares | Collection of resources used in our apps. | +| pandautils | The core library for the apps. All the common code is implemented here that is reused by the 3 apps. | +| rceditor | A wrapper for rich content editing used in our apps. | +| recyclerview | A fancy RecyclerView library that supports expanding and collapsing, pagination, and stuff like that. (deprecated) | #### Our applications are licensed under the GPLv3 License. diff --git a/apps/build.gradle b/apps/build.gradle index e350784c6f..6b0bdfd7ac 100644 --- a/apps/build.gradle +++ b/apps/build.gradle @@ -90,11 +90,13 @@ task assembleAllApps() { apply from: '../gradle/jacoco.gradle' -configurations.all{ - resolutionStrategy { - eachDependency { details -> - if ('org.jacoco' == details.requested.group) { - details.useVersion "0.8.7" +configurations.configureEach { + if (canBeResolved) { + resolutionStrategy { + eachDependency { details -> + if ('org.jacoco' == details.requested.group) { + details.useVersion "0.8.7" + } } } } diff --git a/apps/buildSrc/src/main/java/GlobalDependencies.kt b/apps/buildSrc/src/main/java/GlobalDependencies.kt index 19d8833326..e02e8b72c7 100644 --- a/apps/buildSrc/src/main/java/GlobalDependencies.kt +++ b/apps/buildSrc/src/main/java/GlobalDependencies.kt @@ -199,6 +199,7 @@ object Libs { const val COMPOSE_NAVIGATION_HILT = "androidx.hilt:hilt-navigation-compose:1.3.0" const val COMPOSE_FRAGMENT = "androidx.fragment:fragment-compose:1.8.9" const val COMPOSE_REORDERABLE = "sh.calvin.reorderable:reorderable:${Versions.REORDERABLE}" + const val COMPOSE_FOUNDATION = "androidx.compose.foundation:foundation" // Glance const val GLANCE = "androidx.glance:glance:${Versions.GLANCE}" diff --git a/apps/buildSrc/src/main/java/MergePrivateData.kt b/apps/buildSrc/src/main/java/MergePrivateData.kt index d3e8e4d542..d7c9c6ddab 100644 --- a/apps/buildSrc/src/main/java/MergePrivateData.kt +++ b/apps/buildSrc/src/main/java/MergePrivateData.kt @@ -20,7 +20,7 @@ object PrivateData { val dataDir = File(baseDir, dataDirName).canonicalFile println("") - println("============= MERGE PRIVATE FILES: ${dataDirName.toUpperCase()} ".padEnd(PRINT_PAD_SIZE, '=')) + println("============= MERGE PRIVATE FILES: ${dataDirName.uppercase()} ".padEnd(PRINT_PAD_SIZE, '=')) /* Confirm dir exists */ if (!dataDir.exists() || !dataDir.isDirectory) { diff --git a/apps/parent/build.gradle b/apps/parent/build.gradle index d5bffc8112..1cc57b4fa6 100644 --- a/apps/parent/build.gradle +++ b/apps/parent/build.gradle @@ -26,9 +26,9 @@ plugins { id 'org.jetbrains.kotlin.plugin.compose' } -configurations { - all*.exclude group: 'commons-logging', module: 'commons-logging' - all*.exclude group: 'org.apache.httpcomponents', module: 'httpclient' +configurations.configureEach { + exclude group: 'commons-logging', module: 'commons-logging' + exclude group: 'org.apache.httpcomponents', module: 'httpclient' } def coverageEnabled = project.hasProperty('coverage') @@ -41,8 +41,8 @@ android { applicationId "com.instructure.parentapp" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode 67 - versionName "4.10.1" + versionCode 69 + versionName "4.11.1" buildConfigField "boolean", "IS_TESTING", "false" testInstrumentationRunner 'com.instructure.parentapp.ui.espresso.ParentHiltTestRunner' @@ -123,17 +123,19 @@ android { } } - configurations.all { - resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9' - /* - Resolves dependency versions across test and production APKs, specifically, transitive - dependencies. This is required since Espresso internally has a dependency on support-annotations. - https://github.com/googlecodelabs/android-testing/blob/57852eaf7df88ddaf828eca879a407f2249d5348/app/build.gradle#L86 - */ - resolutionStrategy.force Libs.ANDROIDX_ANNOTATION - - resolutionStrategy.force Libs.KOTLIN_COROUTINES_CORE - resolutionStrategy.force Libs.KOTLIN_STD_LIB + configurations.configureEach { + if (canBeResolved) { + resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9' + /* + Resolves dependency versions across test and production APKs, specifically, transitive + dependencies. This is required since Espresso internally has a dependency on support-annotations. + https://github.com/googlecodelabs/android-testing/blob/57852eaf7df88ddaf828eca879a407f2249d5348/app/build.gradle#L86 + */ + resolutionStrategy.force Libs.ANDROIDX_ANNOTATION + + resolutionStrategy.force Libs.KOTLIN_COROUTINES_CORE + resolutionStrategy.force Libs.KOTLIN_STD_LIB + } } configurations.implementation.dependencies.each { compileDependency -> diff --git a/apps/parent/release_tests/flank_e2e_release.yml b/apps/parent/release_tests/flank_e2e_release.yml new file mode 100644 index 0000000000..12cf2907f6 --- /dev/null +++ b/apps/parent/release_tests/flank_e2e_release.yml @@ -0,0 +1,25 @@ +gcloud: + project: delta-essence-114723 + # Use the next two lines to run locally + # app: ../build/outputs/apk/qa/debug/parent-qa-debug.apk + # test: ../build/outputs/apk/androidTest/qa/debug/parent-qa-debug-androidTest.apk + app: ../apps/parent/build/outputs/apk/qa/debug/parent-qa-debug.apk + test: ../apps/parent/build/outputs/apk/androidTest/qa/debug/parent-qa-debug-androidTest.apk + results-bucket: android-parent + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.ReleaseExclude, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E + device: + - model: Pixel2.arm + version: 29 + locale: en_US + orientation: portrait + +flank: + testShards: 10 + testRuns: 1 diff --git a/apps/parent/release_tests/flank_landscape_interaction_release.yml b/apps/parent/release_tests/flank_landscape_interaction_release.yml new file mode 100644 index 0000000000..ca3e31c1c9 --- /dev/null +++ b/apps/parent/release_tests/flank_landscape_interaction_release.yml @@ -0,0 +1,24 @@ +gcloud: + project: delta-essence-114723 + # Use the next two lines to run locally + # app: ../build/outputs/apk/qa/debug/parent-qa-debug.apk + # test: ../build/outputs/apk/androidTest/qa/debug/parent-qa-debug-androidTest.apk + app: ../apps/parent/build/outputs/apk/qa/debug/parent-qa-debug.apk + test: ../apps/parent/build/outputs/apk/androidTest/qa/debug/parent-qa-debug-androidTest.apk + results-bucket: android-parent + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - notAnnotation com.instructure.canvas.espresso.annotations.ReleaseExclude, com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubLandscape, com.instructure.canvas.espresso.annotations.OfflineE2E + device: + - model: Pixel2.arm + version: 29 + locale: en_US + orientation: landscape + +flank: + testShards: 10 + testRuns: 1 diff --git a/apps/parent/release_tests/flank_portrait_interaction_release.yml b/apps/parent/release_tests/flank_portrait_interaction_release.yml new file mode 100644 index 0000000000..875e3ef3fb --- /dev/null +++ b/apps/parent/release_tests/flank_portrait_interaction_release.yml @@ -0,0 +1,24 @@ +gcloud: + project: delta-essence-114723 + # Use the next two lines to run locally + # app: ../build/intermediates/apk/qa/debug/parent-qa-debug.apk + # test: ../build/intermediates/apk/androidTest/qa/debug/parent-qa-debug-androidTest.apk + app: ../apps/parent/build/outputs/apk/qa/debug/parent-qa-debug.apk + test: ../apps/parent/build/outputs/apk/androidTest/qa/debug/parent-qa-debug-androidTest.apk + results-bucket: android-parent + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - notAnnotation com.instructure.canvas.espresso.annotations.ReleaseExclude, com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E + device: + - model: Pixel2.arm + version: 29 + locale: en_US + orientation: portrait + +flank: + testShards: 10 + testRuns: 1 diff --git a/apps/parent/release_tests/flank_tablet_interaction_release.yml b/apps/parent/release_tests/flank_tablet_interaction_release.yml new file mode 100644 index 0000000000..d17d16dc90 --- /dev/null +++ b/apps/parent/release_tests/flank_tablet_interaction_release.yml @@ -0,0 +1,28 @@ +gcloud: + project: delta-essence-114723 + # Use the next two lines to run locally + # app: ../build/outputs/apk/qa/debug/parent-qa-debug.apk + # test: ../build/outputs/apk/androidTest/qa/debug/parent-qa-debug-androidTest.apk + app: ../apps/parent/build/outputs/apk/qa/debug/parent-qa-debug.apk + test: ../apps/parent/build/outputs/apk/androidTest/qa/debug/parent-qa-debug-androidTest.apk + results-bucket: android-parent + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - notAnnotation com.instructure.canvas.espresso.annotations.ReleaseExclude, com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubTablet, com.instructure.canvas.espresso.annotations.OfflineE2E + device: + - model: MediumTablet.arm + version: 29 + locale: en_US + orientation: landscape + - model: MediumTablet.arm + version: 29 + locale: en_US + orientation: portrait + +flank: + testShards: 10 + testRuns: 1 diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/classic/HelpMenuE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/classic/HelpMenuE2ETest.kt index bc59c57012..e1d6c1a0ea 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/classic/HelpMenuE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/classic/HelpMenuE2ETest.kt @@ -25,7 +25,7 @@ import com.instructure.canvas.espresso.StringConstants.HelpMenu import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.utils.checkToastText import com.instructure.parentapp.R import com.instructure.parentapp.utils.ParentComposeTest import com.instructure.parentapp.utils.extensions.seedData @@ -79,7 +79,7 @@ class HelpMenuE2ETest : ParentComposeTest() { try { helpPage.assertHelpMenuURL(HelpMenu.SEARCH_GUIDES_TITLE, "https://community.instructure.com/en/all-guides") - helpPage.assertHelpMenuURL(HelpMenu.SUBMIT_FEATURE_TITLE, "https://community.canvaslms.com/t5/Idea-Conversations/idb-p/ideas") + helpPage.assertHelpMenuURL(HelpMenu.SHARE_A_CONTRIBUTION_TITLE, "https://community.instructure.com/en/categories/product-connection") helpPage.assertHelpMenuURL(HelpMenu.SHARE_LOVE_TITLE, "market://details?id=com.instructure.parentapp") } finally { diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AlertsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AlertsE2ETest.kt index fa19e001e0..15ec57c6f3 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AlertsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AlertsE2ETest.kt @@ -22,7 +22,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.pressBackButton +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.canvasapi2.models.AlertType import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentDetailsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentDetailsE2ETest.kt index 44392f3d72..efa1f7d6bc 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentDetailsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentDetailsE2ETest.kt @@ -23,7 +23,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.pressBackButton +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.GradingType @@ -132,9 +132,9 @@ class AssignmentDetailsE2ETest : ParentComposeTest() { assignmentDetailsPage.assertDisplayToolbarTitle() assignmentDetailsPage.assertDisplayToolbarSubtitle(course.name) - Log.d(ASSERTION_TAG, "Assert that the assignment status for '${student2.name}' student is 'Not Submitted' and the 'Submission & Rubric' label is displayed and the submission type is 'Text Entry'.") + Log.d(ASSERTION_TAG, "Assert that the assignment status for '${student2.name}' student is 'Not Submitted' and the 'Submission & Feedback' label is displayed and the submission type is 'Text Entry'.") assignmentDetailsPage.assertStatusNotSubmitted() - assignmentDetailsPage.assertSubmissionAndRubricLabel() + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() assignmentDetailsPage.assertSubmissionTypeDisplayed("Text Entry") assignmentDetailsPage.assertReminderViewDisplayed() assignmentDetailsPage.assertNoDescriptionViewDisplayed() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt index 9a62c37a2e..c09800d29d 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt @@ -24,7 +24,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.utils.checkToastText import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CalendarE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CalendarE2ETest.kt index 9f533eef91..680c9e6efe 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CalendarE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CalendarE2ETest.kt @@ -23,7 +23,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.utils.checkToastText import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.api.CalendarEventApi diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsFrontPageE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsFrontPageE2ETest.kt index 9bf77bcd40..f99da7f6ec 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsFrontPageE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsFrontPageE2ETest.kt @@ -16,6 +16,7 @@ package com.instructure.parentapp.ui.e2e.compose import android.util.Log +import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory @@ -32,6 +33,7 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.seedDataForK5 import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -76,7 +78,7 @@ class CourseDetailsFrontPageE2ETest : ParentComposeTest() { dashboardPage.waitForRender() coursesPage.clickCourseItem(course.name) - Log.d(STEP_TAG, "Navigate to Front Page Page by selecting Summary Tab.") + Log.d(STEP_TAG, "Navigate to Front Page Page by selecting Front Page Tab.") courseDetailsPage.selectTab("FRONT PAGE") Log.d(ASSERTION_TAG, "Assert that the 'FRONT PAGE' tab has been selected.") @@ -91,4 +93,58 @@ class CourseDetailsFrontPageE2ETest : ParentComposeTest() { Log.d(ASSERTION_TAG, "Assert that the Assignment Details Page is loaded successfully.") assignmentDetailsPage.assertAssignmentDetails(assignment) } + + @E2E + @Test + @TestMetaData(Priority.COMMON, FeatureCategory.COURSE_DETAILS, TestCategory.E2E, SecondaryFeatureCategory.FRONT_PAGE) + fun testFrontPageTabVisibilityForK5ElementaryCoursesE2E() { + + //Bug Ticket: MBL-19842 + Log.d(PREPARATION_TAG, "Seeding data for K5 sub-account: 2 homeroom courses. The parent observes the K5 elementary student.") + val data = seedDataForK5(students = 1, teachers = 1, parents = 1, homeroomCourses = 2) + val courseWithFrontPage = data.coursesList[0] + val courseWithoutFrontPage = data.coursesList[1] + val teacher = data.teachersList[0] + val parent = data.parentsList[0] + + val syllabusBody = "This is the syllabus body of the K5 elementary course." + val frontPageBody = "This is the front page body of the K5 elementary course." + + Log.d(PREPARATION_TAG, "Seed a front page for '${courseWithFrontPage.name}'.") + PagesApi.createCoursePage(courseWithFrontPage.id, teacher.token, frontPage = true, editingRoles = "public", body = frontPageBody) + + Log.d(PREPARATION_TAG, "Add a syllabus body to both courses so that both get SYLLABUS and SUMMARY tabs.") + CoursesApi.updateCourse(courseWithFrontPage.id, UpdateCourse(syllabusBody = syllabusBody)) + CoursesApi.updateCourse(courseWithoutFrontPage.id, UpdateCourse(syllabusBody = syllabusBody)) + + Log.d(STEP_TAG, "Login with user: '${parent.name}', login id: '${parent.loginId}'.") + tokenLogin(parent) + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Select '${courseWithFrontPage.name}' course.") + dashboardPage.waitForRender() + coursesPage.clickCourseItem(courseWithFrontPage.name) + + Log.d(ASSERTION_TAG, "Assert that '${courseWithFrontPage.name}' has 4 tabs: GRADES, FRONT PAGE, SYLLABUS, SUMMARY.") + courseDetailsPage.assertTabCount(4) + + Log.d(ASSERTION_TAG, "Assert that the 'FRONT PAGE' tab IS displayed for the course that has a front page, even though the course home is NOT set to 'wiki'.") + courseDetailsPage.assertTabDisplayed("FRONT PAGE") + + Log.d(STEP_TAG, "Navigate to the Front Page tab.") + courseDetailsPage.selectTab("FRONT PAGE") + + Log.d(ASSERTION_TAG, "Assert that the 'FRONT PAGE' tab has been selected and the front page body is displayed.") + courseDetailsPage.assertTabSelected("FRONT PAGE") + frontPagePage.assertFrontPageBody(frontPageBody) + + Log.d(STEP_TAG, "Navigate back to the Dashboard and select '${courseWithoutFrontPage.name}' course.") + Espresso.pressBack() + coursesPage.clickCourseItem(courseWithoutFrontPage.name) + + Log.d(ASSERTION_TAG, "Assert that '${courseWithoutFrontPage.name}' has 3 tabs: GRADES, SYLLABUS, SUMMARY — no FRONT PAGE tab since no front page was created.") + courseDetailsPage.assertTabCount(3) + + Log.d(ASSERTION_TAG, "Assert that the 'FRONT PAGE' tab is NOT displayed for the course that has no front page.") + courseDetailsPage.assertTabDoesNotExist("FRONT PAGE") + } } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CustomStatusesE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CustomStatusesE2ETest.kt index 037a036bfc..0f1471e770 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CustomStatusesE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CustomStatusesE2ETest.kt @@ -87,7 +87,7 @@ class CustomStatusesE2ETest: ParentComposeTest() { Log.d(ASSERTION_TAG, "Assert that the Assignment Details Page is displayed with the custom status 'AMAZING'.") assignmentDetailsPage.assertCustomStatus("AMAZING") - assignmentDetailsPage.assertSubmissionAndRubricLabel() + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() } @After diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/DiscussionCheckpointsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/DiscussionCheckpointsE2ETest.kt index 0263cf2287..715ee09a59 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/DiscussionCheckpointsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/DiscussionCheckpointsE2ETest.kt @@ -23,8 +23,12 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.DiscussionTopicsApi +import com.instructure.dataseeding.api.SubmissionsApi +import com.instructure.espresso.convertIso8601ToCanvasFormat import com.instructure.espresso.getCustomDateCalendar +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.pandautils.features.calendar.CalendarPrefs import com.instructure.parentapp.utils.ParentComposeTest import com.instructure.parentapp.utils.extensions.seedData @@ -146,4 +150,99 @@ class DiscussionCheckpointsE2ETest : ParentComposeTest() { assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Additional replies (2) due", assignmentDetailsReplyToEntryDueDate) } + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.E2E, SecondaryFeatureCategory.DISCUSSION_CHECKPOINTS) + fun testDiscussionCheckpointsGradesListE2E() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, parents = 1, teachers = 1, courses = 1) + val teacher = data.teachersList[0] + val parent = data.parentsList[0] + val course = data.coursesList[0] + + val discussionWithCheckpointsTitle = "Test Discussion with Checkpoints" + val assignmentName = "Test Assignment with Checkpoints" + + Log.d(PREPARATION_TAG, "Convert dates to match with different formats in different screens (Grades list, Assignment Details)") + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + val assignmentDetailsDisplayFormat = SimpleDateFormat("MMM d, yyyy h:mm a", Locale.US) + val timeDisplayFormat = SimpleDateFormat("h:mm a", Locale.US) + val replyToTopicCalendar = getCustomDateCalendar(2) + val replyToEntryCalendar = getCustomDateCalendar(4) + val replyToTopicDueDate = dateFormat.format(replyToTopicCalendar.time) + val replyToEntryDueDate = dateFormat.format(replyToEntryCalendar.time) + val assignmentDetailsReplyToTopicDueDate = assignmentDetailsDisplayFormat.format(replyToTopicCalendar.time) + val assignmentDetailsReplyToEntryDueDate = assignmentDetailsDisplayFormat.format(replyToEntryCalendar.time) + val convertedReplyToTopicDueDate = "Due " + convertIso8601ToCanvasFormat(replyToTopicDueDate) + " " + timeDisplayFormat.format(replyToTopicCalendar.time) + val convertedReplyToEntryDueDate = "Due " + convertIso8601ToCanvasFormat(replyToEntryDueDate) + " " + timeDisplayFormat.format(replyToEntryCalendar.time) + + Log.d(PREPARATION_TAG, "Seed a discussion topic with checkpoints for '${course.name}' course.") + DiscussionTopicsApi.createDiscussionTopicWithCheckpoints(course.id, teacher.token, discussionWithCheckpointsTitle, assignmentName, replyToTopicDueDate, replyToEntryDueDate) + + Log.d(STEP_TAG, "Login with user: '${parent.name}', login id: '${parent.loginId}'.") + tokenLogin(parent) + + Log.d(ASSERTION_TAG, "Assert that the Dashboard Page is the landing page and it is loaded successfully.") + dashboardPage.waitForRender() + dashboardPage.assertPageObjects() + + Log.d(STEP_TAG, "Click on the '${course.name}' course.") + coursesPage.clickCourseItem(course.name) + + Log.d(ASSERTION_TAG, "Assert that the details of the course has opened.") + courseDetailsPage.assertCourseNameDisplayed(course) //Course Details Page is actually the Grades page by default when there are no tabs. + + Log.d(ASSERTION_TAG, "Assert that the Grades Card text is 'Total' by default and the 'Based on graded assignments' label is displayed.") + retryWithIncreasingDelay(times = 10, maxDelay = 3000) { + gradesPage.assertCardText("Total") + gradesPage.assertBasedOnGradedAssignmentsLabel() + } + + Log.d(ASSERTION_TAG, "Assert that the '${discussionWithCheckpointsTitle}' discussion is present along with 2 date info (For the 2 checkpoints).") + courseDetailsPage.assertHasAssignmentWithCheckpoints(discussionWithCheckpointsTitle, dueAtString = convertedReplyToTopicDueDate, dueAtStringSecondCheckpoint = convertedReplyToEntryDueDate, expectedGrade = "-/15") + + Log.d(STEP_TAG, "Click on the expand icon for the '$discussionWithCheckpointsTitle' discussion to see the individual checkpoint details.") + courseDetailsPage.clickDiscussionCheckpointExpandCollapseIcon(discussionWithCheckpointsTitle) + + Log.d(ASSERTION_TAG, "Assert that both checkpoints are displayed with the correct due dates and grades after expanding.") + courseDetailsPage.assertDiscussionCheckpointDetails(2, dueAtReplyToTopic = convertedReplyToTopicDueDate, dueAtAdditionalReplies = convertedReplyToEntryDueDate, gradeReplyToTopic = "-/10", gradeAdditionalReplies = "-/5") + + Log.d(STEP_TAG, "Click on the '$discussionWithCheckpointsTitle' discussion item to open the Assignment Details Page.") + courseDetailsPage.clickAssignment(discussionWithCheckpointsTitle) + + Log.d(ASSERTION_TAG, "Assert that the Assignment Details Page is displayed properly with the correct toolbar title and subtitle.") + assignmentDetailsPage.assertDisplayToolbarTitle() + assignmentDetailsPage.assertDisplayToolbarSubtitle(course.name) + + Log.d(ASSERTION_TAG, "Assert that the checkpoints are displayed properly on the Assignment Details Page.") + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Reply to topic due", assignmentDetailsReplyToTopicDueDate) + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Additional replies (2) due", assignmentDetailsReplyToEntryDueDate) + + Log.d(STEP_TAG, "Navigate back to the Course Details Page.") + Espresso.pressBack() + + Log.d(PREPARATION_TAG, "Grade the 'Reply to topic' checkpoint of '$discussionWithCheckpointsTitle' with 1 point via the Teacher API.") + val student = data.studentsList[0] + val parentAssignment = AssignmentsApi.listAssignments(course.id, teacher.token).first() + SubmissionsApi.gradeSubmission(teacher.token, course.id, parentAssignment.id, student.id, postedGrade = "1", subAssignmentTag = "reply_to_topic") + + Log.d(ASSERTION_TAG, "Assert that the total grade of '$discussionWithCheckpointsTitle' is updated to '-/15' after grading the 'Reply to topic' checkpoint.") + courseDetailsPage.assertHasAssignmentWithCheckpoints(discussionWithCheckpointsTitle, dueAtString = convertedReplyToTopicDueDate, dueAtStringSecondCheckpoint = convertedReplyToEntryDueDate, expectedGrade = "-/15") + + Log.d(STEP_TAG, "Click on the expand icon for the '$discussionWithCheckpointsTitle' discussion to see the individual checkpoint details.") + courseDetailsPage.clickDiscussionCheckpointExpandCollapseIcon(discussionWithCheckpointsTitle) + + Log.d(ASSERTION_TAG, "Assert that the 'Reply to topic' grade is '1/10' and the 'Additional replies' grade is '-/5'.") + retryWithIncreasingDelay(times = 15, maxDelay = 3000, catchBlock = { + courseDetailsPage.refresh() + courseDetailsPage.clickDiscussionCheckpointExpandCollapseIcon(discussionWithCheckpointsTitle) + }) + { + courseDetailsPage.assertDiscussionCheckpointDetails(2, dueAtReplyToTopic = convertedReplyToTopicDueDate, statusReplyToTopic = "Graded", dueAtAdditionalReplies = convertedReplyToEntryDueDate, gradeReplyToTopic = "1/10", gradeAdditionalReplies = "-/5") + } + } + } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/InboxE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/InboxE2ETest.kt index a3861bc3b4..042b2aeb9c 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/InboxE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/InboxE2ETest.kt @@ -30,8 +30,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.annotations.ReleaseExclude -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.api.AssignmentsApi @@ -60,7 +59,6 @@ class InboxE2ETest: ParentComposeTest() { @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.INBOX, TestCategory.E2E) - @ReleaseExclude fun testInboxSelectedButtonActionsE2E() { Log.d(PREPARATION_TAG, "Seeding data.") diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/LoginE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/LoginE2ETest.kt index 9c7bfb61fe..9f10104c60 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/LoginE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/LoginE2ETest.kt @@ -290,35 +290,6 @@ class LoginE2ETest : ParentComposeTest() { loginSignInPage.assertLoginEmailErrorMessage(NO_EMAIL_GIVEN_ERROR_MESSAGE) // Invalid credentials error message will be displayed within the email error message holder on the login page. } - private fun loginWithUser(user: CanvasUserApiModel, lastSchoolSaved: Boolean = false) { - - Thread.sleep(5100) //Need to wait > 5 seconds before each login attempt because of new 'too many attempts' login policy on web. - - if (lastSchoolSaved) { - Log.d(STEP_TAG, "Click 'Find Another School' button.") - loginLandingPage.clickFindAnotherSchoolButton() - } else { - Log.d(STEP_TAG, "Click 'Find My School' button.") - loginLandingPage.clickFindMySchoolButton() - } - - Log.d(STEP_TAG, "Enter domain: '${user.domain}'.") - loginFindSchoolPage.enterDomain(user.domain) - - Log.d(STEP_TAG, "Click on 'Next' button on the Toolbar.") - loginFindSchoolPage.clickToolbarNextMenuItem() - loginSignInPage.loginAs(user) - } - - private fun loginWithLastSavedSchool(user: CanvasUserApiModel) { - - Log.d(STEP_TAG, "Click on last saved school's button.") - loginLandingPage.clickOnLastSavedSchoolButton() - - Log.d(STEP_TAG, "Login with '${user.name}' user.") - loginSignInPage.loginAs(user) - } - @E2E @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.LOGIN, TestCategory.E2E) @@ -405,7 +376,6 @@ class LoginE2ETest : ParentComposeTest() { } @Test - @Stub("Stubbed because there was some change on 7th or 8th of July, 2025 and on the CI it loads an invalid URL page, however the test runs locally.") @E2E @TestMetaData(Priority.IMPORTANT, FeatureCategory.LOGIN, TestCategory.E2E, SecondaryFeatureCategory.CANVAS_NETWORK) fun testCanvasNetworkSignInPageE2E() { @@ -486,4 +456,32 @@ class LoginE2ETest : ParentComposeTest() { } } + private fun loginWithUser(user: CanvasUserApiModel, lastSchoolSaved: Boolean = false) { + + Thread.sleep(5100) //Need to wait > 5 seconds before each login attempt because of new 'too many attempts' login policy on web. + + if (lastSchoolSaved) { + Log.d(STEP_TAG, "Click 'Find Another School' button.") + loginLandingPage.clickFindAnotherSchoolButton() + } else { + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + } + + Log.d(STEP_TAG, "Enter domain: '${user.domain}'.") + loginFindSchoolPage.enterDomain(user.domain) + + Log.d(STEP_TAG, "Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + loginSignInPage.loginAs(user) + } + + private fun loginWithLastSavedSchool(user: CanvasUserApiModel) { + + Log.d(STEP_TAG, "Click on last saved school's button.") + loginLandingPage.clickOnLastSavedSchoolButton() + + Log.d(STEP_TAG, "Login with '${user.name}' user.") + loginSignInPage.loginAs(user) + } } \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/SettingsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/SettingsE2ETest.kt index cc1562470e..c4203963d8 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/SettingsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/SettingsE2ETest.kt @@ -24,8 +24,8 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.checkToastText -import com.instructure.canvas.espresso.pressBackButton +import com.instructure.canvas.espresso.utils.checkToastText +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.model.GradingType diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt index 58f168b5fc..b2b3cff577 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -27,7 +27,6 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.checkToastText import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvas.espresso.mockcanvas.addAssignment import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups @@ -35,6 +34,7 @@ import com.instructure.canvas.espresso.mockcanvas.addObserverAlert import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvas.espresso.utils.checkToastText import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.AlertType diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt index d1e19afd22..a735975473 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt @@ -27,7 +27,7 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvas.espresso.mockcanvas.init -import com.instructure.canvas.espresso.waitForMatcherWithSleeps +import com.instructure.canvas.espresso.utils.waitForMatcherWithSleeps import com.instructure.canvasapi2.utils.Pronouns import com.instructure.loginapi.login.R import com.instructure.parentapp.utils.ParentComposeTest diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt index 2da7ba5fae..6ca653b22c 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt @@ -29,7 +29,7 @@ import com.instructure.canvas.espresso.mockcanvas.addEnrollment import com.instructure.canvas.espresso.mockcanvas.addUser import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvas.espresso.mockcanvas.updateUserEnrollments -import com.instructure.canvas.espresso.waitForMatcherWithSleeps +import com.instructure.canvas.espresso.utils.waitForMatcherWithSleeps import com.instructure.canvasapi2.models.Enrollment import com.instructure.loginapi.login.R import com.instructure.parentapp.utils.ParentComposeTest diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt index b498a2e38b..95257f2d1c 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt @@ -12,6 +12,7 @@ import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions import com.instructure.canvas.espresso.mockcanvas.addRecipientsToCourse import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDashboardCoursesManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeRecentGradedSubmissionsManager @@ -27,6 +28,7 @@ import com.instructure.canvasapi2.managers.InboxSettingsManager import com.instructure.canvasapi2.managers.PostPolicyManager import com.instructure.canvasapi2.managers.SubmissionRubricManager import com.instructure.canvasapi2.managers.graphql.AssignmentDetailsManager +import com.instructure.canvasapi2.managers.graphql.DashboardCoursesManager import com.instructure.canvasapi2.managers.graphql.RecentGradedSubmissionsManager import com.instructure.canvasapi2.managers.graphql.SubmissionCommentsManager import com.instructure.canvasapi2.managers.graphql.SubmissionContentManager @@ -102,6 +104,10 @@ class ParentInboxComposeInteractionTest: InboxComposeInteractionTest() { @JvmField val recentGradedSubmissionsManager: RecentGradedSubmissionsManager = FakeRecentGradedSubmissionsManager() + @BindValue + @JvmField + val dashboardCoursesManager: DashboardCoursesManager = FakeDashboardCoursesManager() + @Test fun testParentComposeDefaultValues() { val data = initData(canSendToAll = true) diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt index 18a7389f50..7eb4712d3d 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt @@ -23,6 +23,7 @@ import com.instructure.canvas.espresso.common.interaction.InboxSignatureInteract import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDashboardCoursesManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeRecentGradedSubmissionsManager @@ -38,6 +39,7 @@ import com.instructure.canvasapi2.managers.InboxSettingsManager import com.instructure.canvasapi2.managers.PostPolicyManager import com.instructure.canvasapi2.managers.SubmissionRubricManager import com.instructure.canvasapi2.managers.graphql.AssignmentDetailsManager +import com.instructure.canvasapi2.managers.graphql.DashboardCoursesManager import com.instructure.canvasapi2.managers.graphql.RecentGradedSubmissionsManager import com.instructure.canvasapi2.managers.graphql.SubmissionCommentsManager import com.instructure.canvasapi2.managers.graphql.SubmissionContentManager @@ -98,6 +100,10 @@ class ParentInboxSignatureInteractionTest : InboxSignatureInteractionTest() { @JvmField val recentGradedSubmissionsManager: RecentGradedSubmissionsManager = FakeRecentGradedSubmissionsManager() + @BindValue + @JvmField + val dashboardCoursesManager: DashboardCoursesManager = FakeDashboardCoursesManager() + private val dashboardPage = DashboardPage() private val leftSideNavigationDrawerPage = LeftSideNavigationDrawerPage() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/FrontPagePage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/FrontPagePage.kt index 10c3ec2cb0..1253e65122 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/FrontPagePage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/FrontPagePage.kt @@ -15,6 +15,7 @@ */ package com.instructure.parentapp.ui.pages.classic +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.web.assertion.WebViewAssertions import androidx.test.espresso.web.sugar.Web @@ -27,7 +28,7 @@ import org.hamcrest.Matchers class FrontPagePage : BasePage() { fun assertFrontPageBody(body: String) { - Web.onWebView(withId(R.id.contentWebView)) + Web.onWebView(Matchers.allOf(withId(R.id.contentWebView), isDisplayed())) .withElement(DriverAtoms.findElement(Locator.ID, "content")) .check( WebViewAssertions.webMatches( @@ -38,7 +39,7 @@ class FrontPagePage : BasePage() { } fun clickLink(linkId: String) { - Web.onWebView(withId(R.id.contentWebView)) + Web.onWebView(Matchers.allOf(withId(R.id.contentWebView), isDisplayed())) .withElement(DriverAtoms.findElement(Locator.ID, linkId)) .perform(DriverAtoms.webClick()) } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/HelpPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/HelpPage.kt index bf8dcdff43..f4cb1fa431 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/HelpPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/HelpPage.kt @@ -24,7 +24,7 @@ import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.instructure.canvas.espresso.StringConstants.HelpMenu -import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive import com.instructure.espresso.OnViewWithStringTextIgnoreCase import com.instructure.espresso.OnViewWithText import com.instructure.espresso.assertDisplayed @@ -46,7 +46,7 @@ class HelpPage : BasePage(R.id.helpDialog) { private val reportProblemLabel by OnViewWithStringTextIgnoreCase("Report a Problem") - private val submitFeatureLabel by OnViewWithStringTextIgnoreCase("Submit a Feature Idea") + private val shareContributionLabel by OnViewWithStringTextIgnoreCase("Share a Contribution") private val shareLoveLabel by OnViewWithText(R.string.shareYourLove) @@ -58,8 +58,8 @@ class HelpPage : BasePage(R.id.helpDialog) { reportProblemLabel.scrollTo().click() } - private fun clickSubmitFeatureLabel() { - submitFeatureLabel.scrollTo().click() + private fun clickShareContributionLabel() { + shareContributionLabel.scrollTo().click() } private fun clickShareLoveLabel() { @@ -101,8 +101,8 @@ class HelpPage : BasePage(R.id.helpDialog) { onView(withId(R.id.title) + withText(HelpMenu.REPORT_PROBLEM_TITLE)).assertDisplayed() onView(withId(R.id.subtitle) + withText(HelpMenu.REPORT_PROBLEM_SUBTITLE)).assertDisplayed() - onView(withId(R.id.title) + withText(HelpMenu.SUBMIT_FEATURE_TITLE)).assertDisplayed() - onView(withId(R.id.subtitle) + withText(HelpMenu.SUBMIT_FEATURE_SUBTITLE)).assertDisplayed() + onView(withId(R.id.title) + withText(HelpMenu.SHARE_A_CONTRIBUTION_TITLE)).assertDisplayed() + onView(withId(R.id.subtitle) + withText(HelpMenu.SHARE_A_CONTRIBUTION_SUBTITLE)).assertDisplayed() onView(withId(R.id.title) + withText(HelpMenu.SHARE_LOVE_TITLE)).assertDisplayed() onView(withId(R.id.subtitle) + withText(HelpMenu.SHARE_LOVE_SUBTITLE)).assertDisplayed() @@ -119,7 +119,7 @@ class HelpPage : BasePage(R.id.helpDialog) { when (helpMenuText) { HelpMenu.SEARCH_GUIDES_TITLE -> clickSearchGuidesLabel() - HelpMenu.SUBMIT_FEATURE_TITLE -> clickSubmitFeatureLabel() + HelpMenu.SHARE_A_CONTRIBUTION_TITLE -> clickShareContributionLabel() HelpMenu.SHARE_LOVE_TITLE -> clickShareLoveLabel() } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/LeftSideNavigationDrawerPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/LeftSideNavigationDrawerPage.kt index 4b337d12c8..d87d371dde 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/LeftSideNavigationDrawerPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/LeftSideNavigationDrawerPage.kt @@ -20,7 +20,7 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.waitForMatcherWithSleeps +import com.instructure.canvas.espresso.utils.waitForMatcherWithSleeps import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CourseDetailsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CourseDetailsPage.kt index 31d32d4e8e..548b68f8d4 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CourseDetailsPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CourseDetailsPage.kt @@ -14,7 +14,6 @@ * limitations under the License. * */ - package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.graphics.Color @@ -24,14 +23,18 @@ import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasAnyChild import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasAnySibling import androidx.compose.ui.test.hasParent import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isSelectable import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import com.instructure.canvas.espresso.refresh +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.utils.toDate import com.instructure.dataseeding.model.CourseApiModel @@ -61,6 +64,7 @@ class CourseDetailsPage(private val composeTestRule: ComposeTestRule) { fun selectTab(tabName: String) { composeTestRule.onNodeWithText(tabName).performClick() + composeTestRule.waitForIdle() } fun assertTabSelected(tabName: String) { @@ -80,14 +84,51 @@ class CourseDetailsPage(private val composeTestRule: ComposeTestRule) { composeTestRule.onNodeWithText(assignmentName).assertTextColor(Color(expectedTextColor)) } + fun assertTabDisplayed(tabName: String) { + composeTestRule.onNodeWithText(tabName).assertIsDisplayed() + } + + fun assertTabDoesNotExist(tabName: String) { + composeTestRule.onNodeWithText(tabName).assertDoesNotExist() + } + + fun assertTabCount(expectedCount: Int) { + composeTestRule.onAllNodes(hasAnyAncestor(hasTestTag("courseDetailsTabRow")) and isSelectable()) + .assertCountEquals(expectedCount) + } + fun clickComposeMessageFAB() { composeTestRule.onNodeWithContentDescription("Send a message about this course").performClick() + composeTestRule.waitForIdle() + } + + fun clickDiscussionCheckpointExpandCollapseIcon(discussionTitle: String) { + composeTestRule.onNode(hasTestTag("expandDiscussionCheckpoints") and hasParent(hasAnyDescendant(hasText(discussionTitle))), useUnmergedTree = true) + .performClick() + composeTestRule.waitForIdle() + } + + fun assertDiscussionCheckpointDetails(additionalRepliesCount: Int, dueAtReplyToTopic: String, gradeReplyToTopic: String, statusReplyToTopic: String = "Not Submitted", dueAtAdditionalReplies: String = dueAtReplyToTopic, gradeAdditionalReplies: String = gradeReplyToTopic, statusAdditionalReplies: String = "Not Submitted") { + composeTestRule.onNode(hasTestTag("checkpointName") and hasText("Reply to topic"), useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNode(hasTestTag("checkpointDueDate_Reply to topic") and hasText(dueAtReplyToTopic), true).assertIsDisplayed() + composeTestRule.onNode(hasTestTag("checkpointGradeText") and hasText(gradeReplyToTopic), useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNode(hasTestTag("checkpointSubmissionStateLabel") and hasText(statusReplyToTopic) and hasAnySibling(hasTestTag("checkpointDueDate_Reply to topic")), useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointName") and hasText("Additional replies ($additionalRepliesCount)"), useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNode(hasTestTag("checkpointDueDate_Additional replies ($additionalRepliesCount)") and hasText(dueAtAdditionalReplies), true).assertIsDisplayed() + composeTestRule.onNode(hasTestTag("checkpointGradeText") and hasText(gradeAdditionalReplies), useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNode(hasTestTag("checkpointSubmissionStateLabel") and hasText(statusAdditionalReplies) and hasAnySibling(hasTestTag("checkpointDueDate_Additional replies ($additionalRepliesCount)")), useUnmergedTree = true).assertIsDisplayed() } fun assertHasAssignmentWithCheckpoints(assignmentName: String, dueAtString: String = "No due date", dueAtStringSecondCheckpoint: String? = null, expectedGrade: String? = null) { assertHasAssignmentCommon(assignmentName, dueAtString, dueAtStringSecondCheckpoint, expectedGrade, hasCheckPoints = true) } + fun refresh() { + composeTestRule.onNodeWithTag("gradesList").performTouchInput { swipeDown() } + composeTestRule.waitForIdle() + } + private fun assertHasAssignmentCommon(assignmentName: String, assignmentDueAt: String?, secondCheckpointDueAt: String? = null, expectedGradeLabel: String? = null, assignmentStatus: String? = null, hasCheckPoints : Boolean = false) { // Check if the assignment is a discussion with checkpoints, if yes, we are expecting 2 due dates for the 2 checkpoints. diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/extensions/ParentTestExtensions.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/extensions/ParentTestExtensions.kt index 281d6beec0..121f28270a 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/extensions/ParentTestExtensions.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/extensions/ParentTestExtensions.kt @@ -14,16 +14,19 @@ * along with this program. If not, see . * */ - package com.instructure.parentapp.utils.extensions import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvasapi2.models.User +import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.dataseeding.api.SeedApi +import com.instructure.dataseeding.api.UserApi import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.parentapp.features.login.LoginActivity import com.instructure.parentapp.utils.ParentTest +const val SUB_ACCOUNT_ID = 181364L + fun ParentTest.tokenLogin(user: CanvasUserApiModel) { activityRule.runOnUiThread { @@ -91,3 +94,36 @@ fun seedData( ) return SeedApi.seedData(request) } + +fun seedDataForK5( + teachers: Int = 0, + courses: Int = 0, + students: Int = 0, + parents: Int = 0, + homeroomCourses: Int = 0, + announcements: Int = 0 +): SeedApi.SeededDataApiModel { + + val request = SeedApi.SeedDataRequest( + teachers = teachers, + students = students, + courses = courses, + homeroomCourses = homeroomCourses, + announcements = announcements, + accountId = SUB_ACCOUNT_ID + ) + val data = SeedApi.seedDataForSubAccount(request) + + val observedStudents = data.studentsList.take(students) + repeat(parents) { + val parent = UserApi.createCanvasUser() + data.addParents(parent) + observedStudents.forEach { student -> + data.coursesList.forEach { course -> + data.addEnrollments(EnrollmentsApi.enrollUserAsObserver(course.id, parent.id, student.id)) + } + } + } + + return data +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/DefaultBindingsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/DefaultBindingsModule.kt index 72929c4b05..f32b780950 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/DefaultBindingsModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/DefaultBindingsModule.kt @@ -22,11 +22,10 @@ import com.instructure.pandautils.features.dashboard.edit.EditDashboardRepositor import com.instructure.pandautils.features.dashboard.edit.EditDashboardRouter import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter import com.instructure.pandautils.features.dashboard.widget.conferences.ConferencesWidgetRouter -import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetBehavior import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetRouter import com.instructure.pandautils.features.dashboard.widget.forecast.ForecastWidgetRouter import com.instructure.pandautils.features.dashboard.widget.progress.ProgressWidgetRouter -import com.instructure.pandautils.features.dashboard.widget.todo.TodoWidgetBehavior +import com.instructure.pandautils.features.dashboard.widget.todo.TodoHomeScreenWidgetUpdater import com.instructure.pandautils.features.dashboard.widget.todo.TodoWidgetRouter import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragmentBehavior import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperRepository @@ -141,11 +140,6 @@ class DefaultBindingsModule { throw NotImplementedError() } - @Provides - fun provideCoursesWidgetBehavior(): CoursesWidgetBehavior { - throw NotImplementedError() - } - @Provides fun provideForecastWidgetRouter(): ForecastWidgetRouter { throw NotImplementedError() @@ -167,7 +161,7 @@ class DefaultBindingsModule { } @Provides - fun provideTodoWidgetBehavior(): TodoWidgetBehavior { + fun provideTodoHomeScreenWidgetUpdater(): TodoHomeScreenWidgetUpdater { throw NotImplementedError() } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CookieConsentModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CookieConsentModule.kt new file mode 100644 index 0000000000..8bd22d4b3b --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CookieConsentModule.kt @@ -0,0 +1,59 @@ +/* + * 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 . + */ +package com.instructure.parentapp.di.feature + +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.ConsentPrefs +import com.instructure.pandautils.features.cookieconsent.AnalyticsConsentHandler +import com.instructure.pandautils.features.cookieconsent.CookieConsentNamespace +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.PendoTokenConfig +import com.instructure.parentapp.BuildConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class CookieConsentModule { + + @Provides + fun provideCookieConsentNamespace(): CookieConsentNamespace { + return CookieConsentNamespace.PARENT + } + + @Provides + @Singleton + fun providePendoTokenConfig(): PendoTokenConfig { + return PendoTokenConfig( + fallbackToken = BuildConfig.PENDO_TOKEN, + apiTokenSelector = { it.pendoMobileParentApiKey } + ) + } + + @Provides + fun provideAnalyticsConsentHandler( + userApi: UserAPI.UsersInterface, + featureFlagProvider: FeatureFlagProvider, + consentPrefs: ConsentPrefs, + apiPrefs: ApiPrefs + ): AnalyticsConsentHandler { + return object : AnalyticsConsentHandler(userApi, featureFlagProvider, consentPrefs, apiPrefs) {} + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LoginModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LoginModule.kt index 92b23e3f60..3a16d51efd 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LoginModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LoginModule.kt @@ -26,9 +26,11 @@ import com.instructure.canvasapi2.utils.Analytics import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.loginapi.login.LoginNavigation import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter +import com.instructure.loginapi.login.features.cookieconsent.CookieConsentRouter import com.instructure.loginapi.login.util.LoginPrefs import com.instructure.pandautils.features.reminder.AlarmScheduler import com.instructure.parentapp.features.login.ParentAcceptableUsePolicyRouter +import com.instructure.parentapp.features.login.ParentCookieConsentRouter import com.instructure.parentapp.features.login.ParentLoginNavigation import com.instructure.parentapp.features.login.SignInActivity import dagger.Module @@ -59,6 +61,11 @@ class LoginModule { ): LoginNavigation { return ParentLoginNavigation(activity, alarmScheduler) } + + @Provides + fun provideCookieConsentRouter(activity: FragmentActivity): CookieConsentRouter { + return ParentCookieConsentRouter(activity) + } } @Module diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SplashModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SplashModule.kt index 9281cef0fb..45ad36c769 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SplashModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SplashModule.kt @@ -36,9 +36,8 @@ class SplashModule { fun provideSplashRepository( userApi: UserAPI.UsersInterface, themeApi: ThemeAPI.ThemeInterface, - enrollmentApi: EnrollmentAPI.EnrollmentInterface, - featuresApi: FeaturesAPI.FeaturesInterface + enrollmentApi: EnrollmentAPI.EnrollmentInterface ): SplashRepository { - return SplashRepository(userApi, themeApi, enrollmentApi, featuresApi) + return SplashRepository(userApi, themeApi, enrollmentApi) } } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRouter.kt index 6e58aa5199..a7a6f254b4 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRouter.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRouter.kt @@ -22,6 +22,7 @@ import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.apis.InboxApi import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Conversation +import com.instructure.canvasapi2.models.MediaComment import com.instructure.pandautils.features.inbox.list.InboxRouter import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions import com.instructure.pandautils.utils.FileDownloader @@ -63,6 +64,10 @@ class ParentInboxRouter( fileDownloader.downloadFileToDevice(attachment) } + override fun routeToMediaAttachment(mediaComment: MediaComment) { + fileDownloader.downloadFileToDevice(mediaComment.url, mediaComment.displayName, mediaComment.contentType) + } + override fun popDetailsScreen(activity: FragmentActivity?) { activity?.onBackPressed() } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/login/ParentAcceptableUsePolicyRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/login/ParentAcceptableUsePolicyRouter.kt index 20d973531e..403484184c 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/login/ParentAcceptableUsePolicyRouter.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/login/ParentAcceptableUsePolicyRouter.kt @@ -22,10 +22,10 @@ import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.utils.Analytics import com.instructure.canvasapi2.utils.AnalyticsEventConstants import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter +import com.instructure.loginapi.login.features.cookieconsent.CookieConsentActivity import com.instructure.loginapi.login.tasks.LogoutTask import com.instructure.pandautils.features.reminder.AlarmScheduler import com.instructure.parentapp.R -import com.instructure.parentapp.features.main.MainActivity import com.instructure.parentapp.features.webview.HtmlContentActivity import com.instructure.parentapp.util.ParentLogoutTask @@ -42,8 +42,9 @@ class ParentAcceptableUsePolicyRouter( override fun startApp() { CookieManager.getInstance().flush() - val intent = Intent(activity, MainActivity::class.java) + val intent = Intent(activity, CookieConsentActivity::class.java) activity.intent?.extras?.let { intent.putExtras(it) } + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK activity.startActivity(intent) } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/login/ParentCookieConsentRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/login/ParentCookieConsentRouter.kt new file mode 100644 index 0000000000..98c66750f3 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/login/ParentCookieConsentRouter.kt @@ -0,0 +1,33 @@ +/* + * 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 . + */ +package com.instructure.parentapp.features.login + +import android.content.Intent +import androidx.fragment.app.FragmentActivity +import com.instructure.loginapi.login.features.cookieconsent.CookieConsentRouter +import com.instructure.parentapp.features.main.MainActivity + +class ParentCookieConsentRouter( + private val activity: FragmentActivity +) : CookieConsentRouter { + + override fun startApp() { + val intent = Intent(activity, MainActivity::class.java) + activity.intent?.extras?.let { intent.putExtras(it) } + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + activity.startActivity(intent) + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorAction.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorAction.kt index 32385dabf0..20d7717bea 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorAction.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorAction.kt @@ -29,4 +29,5 @@ sealed class RouteValidatorAction { data class ShowToast(val message: String) : RouteValidatorAction() data class StartSignInActivity(val accountDomain: AccountDomain) : RouteValidatorAction() data object StartLoginActivity : RouteValidatorAction() + data object StartCookieConsent : RouteValidatorAction() } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorActivity.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorActivity.kt index bb7a1081df..5d3f44dd4c 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorActivity.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorActivity.kt @@ -30,6 +30,7 @@ import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.collectOneOffEvents import com.instructure.parentapp.databinding.ActivityRouteValidatorBinding +import com.instructure.loginapi.login.features.cookieconsent.CookieConsentActivity import com.instructure.parentapp.features.login.LoginActivity import com.instructure.parentapp.features.login.SignInActivity import com.instructure.parentapp.features.main.MainActivity @@ -60,6 +61,7 @@ class RouteValidatorActivity : BaseCanvasActivity() { is RouteValidatorAction.ShowToast -> Toast.makeText(this, action.message, Toast.LENGTH_LONG).show() is RouteValidatorAction.StartSignInActivity -> startSignInActivity(action.accountDomain) is RouteValidatorAction.StartLoginActivity -> startLoginActivity() + is RouteValidatorAction.StartCookieConsent -> startCookieConsent() } } @@ -92,6 +94,13 @@ class RouteValidatorActivity : BaseCanvasActivity() { finish() } + private fun startCookieConsent() { + val intent = Intent(this, CookieConsentActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + finish() + } + companion object { fun createIntent(context: Context, uri: Uri): Intent { val intent = Intent(context, RouteValidatorActivity::class.java) diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorViewModel.kt index 63448e453d..51b7c66876 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorViewModel.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/login/routevalidator/RouteValidatorViewModel.kt @@ -108,7 +108,7 @@ class RouteValidatorViewModel @Inject constructor( } else { // Log the analytics - only for real logins, not masquerading logQREvent(apiPrefs.domain, true) - postActionWithDelay(RouteValidatorAction.StartMainActivity()) + postActionWithDelay(RouteValidatorAction.StartCookieConsent) } return@tryLaunch } catch (e: Throwable) { diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviour.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviour.kt index 16a0c73b6b..cd19f74c9c 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviour.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviour.kt @@ -26,7 +26,7 @@ class ParentSettingsBehaviour(private val selectedStudentHolder: SelectedStudent get() = mapOf( R.string.preferences to listOf(SettingsItem.APP_THEME), R.string.inboxSettingsTitle to listOf(SettingsItem.INBOX_SIGNATURE), - R.string.legal to listOf(SettingsItem.ABOUT, SettingsItem.LEGAL) + R.string.legal to listOf(SettingsItem.ABOUT, SettingsItem.LEGAL, SettingsItem.PRIVACY) ) override suspend fun applyAppSpecificColorSettings() { diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsRouter.kt index 09814a3b77..b8dd82c587 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsRouter.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsRouter.kt @@ -24,4 +24,8 @@ class ParentSettingsRouter(private val navigation: Navigation, private val activ override fun navigateToInboxSignature() { navigation.navigate(activity, navigation.inboxSignatureSettings) } + + override fun navigateToPrivacySettings() { + navigation.navigate(activity, navigation.privacySettings) + } } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashRepository.kt index 168868afdd..3278ff7ded 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashRepository.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashRepository.kt @@ -18,23 +18,19 @@ package com.instructure.parentapp.features.splash import com.instructure.canvasapi2.apis.EnrollmentAPI -import com.instructure.canvasapi2.apis.FeaturesAPI import com.instructure.canvasapi2.apis.ThemeAPI import com.instructure.canvasapi2.apis.UserAPI import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.managers.FeaturesManager import com.instructure.canvasapi2.models.CanvasColor import com.instructure.canvasapi2.models.CanvasTheme import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.depaginate -import com.instructure.pandautils.utils.orDefault class SplashRepository( private val userApi: UserAPI.UsersInterface, private val themeApi: ThemeAPI.ThemeInterface, - private val enrollmentApi: EnrollmentAPI.EnrollmentInterface, - private val featuresApi: FeaturesAPI.FeaturesInterface + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface ) { suspend fun getSelf(): User? { @@ -70,9 +66,4 @@ class SplashRepository( val params = RestParams(isForceReadFromNetwork = true) return userApi.getBecomeUserPermission(params).dataOrNull?.becomeUser ?: false } - - suspend fun getSendUsageMetrics(): Boolean { - val params = RestParams(isForceReadFromNetwork = true) - return featuresApi.getEnvironmentFeatureFlags(params).dataOrNull?.get(FeaturesManager.SEND_USAGE_METRICS).orDefault() - } } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt index c2ca03f046..f3b26c5365 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt @@ -25,16 +25,15 @@ import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch +import com.instructure.pandautils.domain.usecase.splash.SetupPendoTrackingUseCase import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.FeatureFlagProvider -import com.instructure.pandautils.utils.SHA256 import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import sdk.pendo.io.Pendo import javax.inject.Inject @@ -45,6 +44,7 @@ class SplashViewModel @Inject constructor( private val apiPrefs: ApiPrefs, private val colorKeeper: ColorKeeper, private val featureFlagProvider: FeatureFlagProvider, + private val setupPendoTrackingUseCase: SetupPendoTrackingUseCase, savedStateHandle: SavedStateHandle ) : ViewModel() { @@ -83,19 +83,7 @@ class SplashViewModel @Inject constructor( } } - val sendUsageMetrics = repository.getSendUsageMetrics() - if (sendUsageMetrics) { - val userWithIds = repository.getSelfWithUuid() - val visitorData = mapOf( - "locale" to apiPrefs.effectiveLocale, - ) - val accountData = mapOf( - "surveyOptOut" to featureFlagProvider.checkAccountSurveyNotificationsFlag() - ) - Pendo.startSession(userWithIds?.uuid?.SHA256().orEmpty(), userWithIds?.accountUuid.orEmpty(), visitorData, accountData) - } else { - Pendo.endSession() - } + setupPendoTrackingUseCase(Unit) val students = repository.getStudents() if (students.isEmpty() && apiPrefs.canBecomeUser == false) { diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/webview/SimpleWebViewFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/webview/SimpleWebViewFragment.kt index b9edc48c44..861a69e8f1 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/webview/SimpleWebViewFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/webview/SimpleWebViewFragment.kt @@ -174,6 +174,20 @@ class SimpleWebViewFragment : BaseCanvasFragment(), NavigationCallbacks { } } + webView.setMediaDownloadCallback(object : CanvasWebView.MediaDownloadCallback { + override fun downloadMedia(mime: String?, url: String?, filename: String?) { + if (!limitWebAccess) { + viewModel.downloadFile(mime.orEmpty(), url.orEmpty(), filename.orEmpty()) + } + } + + override fun downloadInternalMedia(mime: String?, url: String?, filename: String?) { + if (!limitWebAccess) { + viewModel.downloadFile(mime.orEmpty(), url.orEmpty(), filename.orEmpty()) + } + } + }) + webView.loadUrl(mainUrl) } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/util/AppManager.kt b/apps/parent/src/main/java/com/instructure/parentapp/util/AppManager.kt index 0efb1d3334..d74336b31f 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/util/AppManager.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/util/AppManager.kt @@ -20,9 +20,7 @@ package com.instructure.parentapp.util import androidx.hilt.work.HiltWorkerFactory import com.instructure.loginapi.login.tasks.LogoutTask import com.instructure.pandautils.features.reminder.AlarmScheduler -import com.instructure.parentapp.BuildConfig import dagger.hilt.android.HiltAndroidApp -import sdk.pendo.io.Pendo import javax.inject.Inject @@ -38,11 +36,6 @@ class AppManager : BaseAppManager() { @Inject lateinit var flutterAppMigration: FlutterAppMigration - override fun onCreate() { - super.onCreate() - initPendo() - } - override fun performLogoutOnAuthError() { ParentLogoutTask(LogoutTask.Type.LOGOUT, null, getScheduler()).execute() } @@ -57,8 +50,4 @@ class AppManager : BaseAppManager() { flutterAppMigration.migrateIfNecessary() } - private fun initPendo() { - val options = Pendo.PendoOptions.Builder().setJetpackComposeBeta(true).build() - Pendo.setup(this, BuildConfig.PENDO_TOKEN, options, null) - } } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt index 48429754f4..1f729130b1 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt @@ -28,6 +28,7 @@ import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.features.settings.SettingsFragment import com.instructure.pandautils.features.settings.inboxsignature.InboxSignatureFragment +import com.instructure.pandautils.features.privacysettings.PrivacySettingsFragment import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.fromJson import com.instructure.pandautils.utils.toJson @@ -80,6 +81,7 @@ class Navigation(apiPrefs: ApiPrefs) { val qrPairing = "$baseUrl/qr-pairing" val settings = "$baseUrl/settings" val inboxSignatureSettings = "$baseUrl/inboxSignatureSettings" + val privacySettings = "$baseUrl/privacySettings" private fun splashRoute(qrCodeMasqueradeId: Long) = "$baseUrl/splash/$qrCodeMasqueradeId" fun assignmentDetailsRoute(courseId: Long, assignmentId: Long) = "$baseUrl/courses/${courseId}/assignments/${assignmentId}" @@ -280,6 +282,7 @@ class Navigation(apiPrefs: ApiPrefs) { } } fragment(inboxSignatureSettings) + fragment(privacySettings) } } diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviourTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviourTest.kt index e471698479..03c0d6e542 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviourTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviourTest.kt @@ -38,7 +38,8 @@ class ParentSettingsBehaviourTest { R.string.inboxSettingsTitle to listOf(SettingsItem.INBOX_SIGNATURE), R.string.legal to listOf( SettingsItem.ABOUT, - SettingsItem.LEGAL + SettingsItem.LEGAL, + SettingsItem.PRIVACY ) ) diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashRepositoryTest.kt index 978f7c49fb..17b06b9def 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashRepositoryTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashRepositoryTest.kt @@ -18,11 +18,9 @@ package com.instructure.parentapp.features.splash import com.instructure.canvasapi2.apis.EnrollmentAPI -import com.instructure.canvasapi2.apis.FeaturesAPI import com.instructure.canvasapi2.apis.ThemeAPI import com.instructure.canvasapi2.apis.UserAPI import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.managers.FeaturesManager import com.instructure.canvasapi2.models.BecomeUserPermission import com.instructure.canvasapi2.models.CanvasColor import com.instructure.canvasapi2.models.CanvasTheme @@ -45,9 +43,8 @@ class SplashRepositoryTest { private val themeApi: ThemeAPI.ThemeInterface = mockk(relaxed = true) private val userApi: UserAPI.UsersInterface = mockk(relaxed = true) private val enrollmentApi: EnrollmentAPI.EnrollmentInterface = mockk(relaxed = true) - private val featuresApi: FeaturesAPI.FeaturesInterface = mockk(relaxed = true) - private val repository = SplashRepository(userApi, themeApi, enrollmentApi, featuresApi) + private val repository = SplashRepository(userApi, themeApi, enrollmentApi) @Test fun `Get students successfully returns data`() = runTest { @@ -155,34 +152,6 @@ class SplashRepositoryTest { assertFalse(result) } - @Test - fun `Get send usage metrics returns false when feature flag is disabled`() = runTest { - coEvery { featuresApi.getEnvironmentFeatureFlags(any()) } returns DataResult.Success( - mapOf(FeaturesManager.SEND_USAGE_METRICS to false) - ) - - val result = repository.getSendUsageMetrics() - assertFalse(result) - } - - @Test - fun `Get send usage metrics returns true when feature flag is enabled`() = runTest { - coEvery { featuresApi.getEnvironmentFeatureFlags(any()) } returns DataResult.Success( - mapOf(FeaturesManager.SEND_USAGE_METRICS to true) - ) - - val result = repository.getSendUsageMetrics() - assertTrue(result) - } - - @Test - fun `Get send usage metrics returns false when call fails`() = runTest { - coEvery { featuresApi.getEnvironmentFeatureFlags(any()) } returns DataResult.Fail() - - val result = repository.getSendUsageMetrics() - assertFalse(result) - } - @Test fun `Get user with UUID successfully returns data`() = runTest { val expected = User(id = 1L, uuid = "uuid", accountUuid = "accountUuid") diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt index c1685dd2ef..f474cb937d 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt @@ -28,6 +28,7 @@ import com.instructure.canvasapi2.models.CanvasTheme import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.pandautils.domain.usecase.splash.SetupPendoTrackingUseCase import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.FeatureFlagProvider @@ -35,7 +36,6 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic import io.mockk.unmockkAll import io.mockk.verify import kotlinx.coroutines.Dispatchers @@ -51,7 +51,6 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test -import sdk.pendo.io.Pendo @ExperimentalCoroutinesApi @@ -70,6 +69,7 @@ class SplashViewModelTest { private val colorKeeper: ColorKeeper = mockk(relaxed = true) private val savedStateHandle = mockk(relaxed = true) private val featureFlagProvider = mockk(relaxed = true) + private val setupPendoTrackingUseCase = mockk(relaxed = true) private lateinit var viewModel: SplashViewModel @@ -80,9 +80,6 @@ class SplashViewModelTest { lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) Dispatchers.setMain(testDispatcher) ContextKeeper.appContext = context - mockkStatic(Pendo::class) - every { Pendo.startSession(any(), any(), any(), any()) } returns Unit - every { Pendo.endSession() } returns Unit } @After @@ -259,30 +256,14 @@ class SplashViewModelTest { verify(exactly = 0) { apiPrefs.canBecomeUser = any() } } - @Test - fun `Send usage metrics enabled`() = runTest { - coEvery { repository.getSendUsageMetrics() } returns true - - createViewModel() - - backgroundScope.launch(testDispatcher) { - viewModel.events.toList() - } - - verify { Pendo.startSession(any(), any(), any(), any()) } - } - - @Test - fun `Send usage metrics disabled`() = runTest { - coEvery { repository.getSendUsageMetrics() } returns false - + fun `Setup pendo tracking use case is invoked on load`() = runTest { createViewModel() backgroundScope.launch(testDispatcher) { viewModel.events.toList() } - verify { Pendo.endSession() } + coVerify { setupPendoTrackingUseCase(Unit) } } private fun createViewModel() { @@ -292,6 +273,7 @@ class SplashViewModelTest { apiPrefs = apiPrefs, colorKeeper = colorKeeper, featureFlagProvider = featureFlagProvider, + setupPendoTrackingUseCase = setupPendoTrackingUseCase, savedStateHandle = savedStateHandle ) } diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/webview/SimpleWebViewRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/webview/SimpleWebViewRepositoryTest.kt index 75b2aa9f1d..750b05fefe 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/webview/SimpleWebViewRepositoryTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/webview/SimpleWebViewRepositoryTest.kt @@ -39,7 +39,7 @@ class SimpleWebViewRepositoryTest { fun `Get authenticated session successfully returns data`() = runTest { val expected = "sessionUrl" - coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Success(AuthenticatedSession(sessionUrl = expected)) + coEvery { oAuthApi.getAuthenticatedSession(any(), any(), any()) } returns DataResult.Success(AuthenticatedSession(sessionUrl = expected)) val result = repository.getAuthenticatedSession("url") Assert.assertEquals(expected, result) @@ -47,7 +47,7 @@ class SimpleWebViewRepositoryTest { @Test(expected = IllegalStateException::class) fun `Get authenticated session fails throws exception`() = runTest { - coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Fail() + coEvery { oAuthApi.getAuthenticatedSession(any(), any(), any()) } returns DataResult.Fail() repository.getAuthenticatedSession("url") } diff --git a/apps/parent/src/test/java/com/instructure/parentapp/login/routevalidator/RouteValidatorViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/login/routevalidator/RouteValidatorViewModelTest.kt index 94f0c3003c..91302e5e0f 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/login/routevalidator/RouteValidatorViewModelTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/login/routevalidator/RouteValidatorViewModelTest.kt @@ -153,7 +153,7 @@ class RouteValidatorViewModelTest { every { apiPrefs.getValidToken() } returns "" every { qrLogin.verifySSOLoginUri(any()) } returns true coEvery { qrLogin.performSSOLogin(any(), any(), any()) } returns OAuthTokenResponse() - coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Success(AuthenticatedSession("sessionUrl")) + coEvery { oAuthApi.getAuthenticatedSession(any(), any(), any()) } returns DataResult.Success(AuthenticatedSession("sessionUrl")) createViewModel() @@ -166,7 +166,7 @@ class RouteValidatorViewModelTest { Assert.assertEquals(RouteValidatorAction.LoadWebViewUrl("sessionUrl"), events.last()) delay(800) - Assert.assertEquals(RouteValidatorAction.StartMainActivity(), events.last()) + Assert.assertEquals(RouteValidatorAction.StartCookieConsent, events.last()) } @Test @@ -177,7 +177,7 @@ class RouteValidatorViewModelTest { realUser = TokenUser(1, "", ""), user = TokenUser(1, "", "") ) - coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Success(AuthenticatedSession("sessionUrl")) + coEvery { oAuthApi.getAuthenticatedSession(any(), any(), any()) } returns DataResult.Success(AuthenticatedSession("sessionUrl")) createViewModel() diff --git a/apps/settings.gradle b/apps/settings.gradle index 99bd1d48a7..8c64adf2d4 100644 --- a/apps/settings.gradle +++ b/apps/settings.gradle @@ -23,23 +23,25 @@ include ':canvas-api-2' include ':dataseedingapi' include ':espresso' include ':interactions' -include ":jazzyviewpager" include ':login-api-2' include ':pandautils' include ':rceditor' include ':recyclerview' include ':pandares' include ':horizon' +include ':ngc' +include ':instui' project(':annotations').projectDir = new File(rootProject.projectDir, '/../libs/annotations') project(':canvas-api-2').projectDir = new File(rootProject.projectDir, '/../libs/canvas-api-2') project(':dataseedingapi').projectDir = new File(rootProject.projectDir, '/../automation/dataseedingapi') project(':espresso').projectDir = new File(rootProject.projectDir, '/../automation/espresso') project(':interactions').projectDir = new File(rootProject.projectDir, '/../libs/interactions') -project(':jazzyviewpager').projectDir = new File(rootProject.projectDir, '/../libs/jazzyviewpager') project(':login-api-2').projectDir = new File(rootProject.projectDir, '/../libs/login-api-2') project(':pandautils').projectDir = new File(rootProject.projectDir, '/../libs/pandautils') project(':rceditor').projectDir = new File(rootProject.projectDir, '/../libs/rceditor') project(':recyclerview').projectDir = new File(rootProject.projectDir, '/../libs/recyclerview') project(':pandares').projectDir = new File(rootProject.projectDir, '/../libs/pandares') project(':horizon').projectDir = new File(rootProject.projectDir, '/../libs/horizon') +project(':ngc').projectDir = new File(rootProject.projectDir, '/../libs/ngc') +project(':instui').projectDir = new File(rootProject.projectDir, '/../libs/instui') diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 27bd9e9bf2..ba11bd15a7 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -25,9 +25,9 @@ apply plugin: 'org.jetbrains.kotlin.plugin.compose' def updatePriority = 2 def coverageEnabled = project.hasProperty('coverage') -configurations { - all*.exclude group: 'commons-logging', module: 'commons-logging' - all*.exclude group: 'org.apache.httpcomponents', module: 'httpclient' +configurations.configureEach { + exclude group: 'commons-logging', module: 'commons-logging' + exclude group: 'org.apache.httpcomponents', module: 'httpclient' } android { @@ -38,8 +38,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 289 - versionName = '8.6.1' + versionCode = 291 + versionName = '8.7.1' vectorDrawables.useSupportLibrary = true testInstrumentationRunner 'com.instructure.student.espresso.StudentHiltTestRunner' @@ -166,17 +166,19 @@ android { testOptions.unitTests.includeAndroidResources = true testOptions.animationsDisabled = true - configurations.all { - resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9' - /* - Resolves dependency versions across test and production APKs, specifically, transitive - dependencies. This is required since Espresso internally has a dependency on support-annotations. - https://github.com/googlecodelabs/android-testing/blob/57852eaf7df88ddaf828eca879a407f2249d5348/app/build.gradle#L86 - */ - resolutionStrategy.force Libs.ANDROIDX_ANNOTATION - - resolutionStrategy.force Libs.KOTLIN_COROUTINES_CORE - resolutionStrategy.force Libs.KOTLIN_STD_LIB + configurations.configureEach { + if (canBeResolved) { + resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9' + /* + Resolves dependency versions across test and production APKs, specifically, transitive + dependencies. This is required since Espresso internally has a dependency on support-annotations. + https://github.com/googlecodelabs/android-testing/blob/57852eaf7df88ddaf828eca879a407f2249d5348/app/build.gradle#L86 + */ + resolutionStrategy.force Libs.ANDROIDX_ANNOTATION + + resolutionStrategy.force Libs.KOTLIN_COROUTINES_CORE + resolutionStrategy.force Libs.KOTLIN_STD_LIB + } } /* @@ -237,6 +239,7 @@ dependencies { implementation project(path: ':rceditor') implementation project(path: ':interactions') implementation project(path: ':horizon') + implementation project(path: ':ngc') /* Android Test Dependencies */ androidTestImplementation project(path: ':espresso') diff --git a/apps/student/release_tests/flank_e2e_offline_release.yml b/apps/student/release_tests/flank_e2e_offline_release.yml new file mode 100644 index 0000000000..5dbbcd188c --- /dev/null +++ b/apps/student/release_tests/flank_e2e_offline_release.yml @@ -0,0 +1,26 @@ +gcloud: + project: delta-essence-114723 +# Use the next two lines to run locally +# app: ../build/outputs/apk/qa/debug/student-qa-debug.apk +# test: ../build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + app: ../apps/student/build/outputs/apk/qa/debug/student-qa-debug.apk + test: ../apps/student/build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + results-bucket: android-student + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - annotation com.instructure.canvas.espresso.annotations.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.ReleaseExclude, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug + device: + - model: Pixel2.arm + version: 29 + locale: en_US + orientation: portrait + +flank: + testShards: 10 + testRuns: 1 + diff --git a/apps/student/release_tests/flank_e2e_release.yml b/apps/student/release_tests/flank_e2e_release.yml new file mode 100644 index 0000000000..29e6ae0451 --- /dev/null +++ b/apps/student/release_tests/flank_e2e_release.yml @@ -0,0 +1,25 @@ +gcloud: + project: delta-essence-114723 +# Use the next two lines to run locally +# app: ../build/outputs/apk/qa/debug/student-qa-debug.apk +# test: ../build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + app: ../apps/student/build/outputs/apk/qa/debug/student-qa-debug.apk + test: ../apps/student/build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + results-bucket: android-student + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.ReleaseExclude, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E + device: + - model: Pixel2.arm + version: 29 + locale: en_US + orientation: portrait + +flank: + testShards: 10 + testRuns: 1 diff --git a/apps/student/release_tests/flank_landscape_interaction_release.yml b/apps/student/release_tests/flank_landscape_interaction_release.yml new file mode 100644 index 0000000000..d71fa6399d --- /dev/null +++ b/apps/student/release_tests/flank_landscape_interaction_release.yml @@ -0,0 +1,25 @@ +gcloud: + project: delta-essence-114723 +# Use the next two lines to run locally +# app: ../build/outputs/apk/qa/debug/student-qa-debug.apk +# test: ../build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + app: ../apps/student/build/outputs/apk/qa/debug/student-qa-debug.apk + test: ../apps/student/build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + results-bucket: android-student + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - notAnnotation com.instructure.canvas.espresso.annotations.ReleaseExclude, com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubLandscape, com.instructure.canvas.espresso.annotations.OfflineE2E + device: + - model: Pixel2.arm + version: 29 + locale: en_US + orientation: landscape + +flank: + testShards: 10 + testRuns: 1 + diff --git a/apps/student/release_tests/flank_portrait_interaction_release.yml b/apps/student/release_tests/flank_portrait_interaction_release.yml new file mode 100644 index 0000000000..af54f37f10 --- /dev/null +++ b/apps/student/release_tests/flank_portrait_interaction_release.yml @@ -0,0 +1,25 @@ +gcloud: + project: delta-essence-114723 +# Use the next two lines to run locally +# app: ../build/intermediates/apk/qa/debug/student-qa-debug.apk +# test: ../build/intermediates/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + app: ../apps/student/build/outputs/apk/qa/debug/student-qa-debug.apk + test: ../apps/student/build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + results-bucket: android-student + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - notAnnotation com.instructure.canvas.espresso.annotations.ReleaseExclude, com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E + device: + - model: Pixel2.arm + version: 29 + locale: en_US + orientation: portrait + +flank: + testShards: 10 + testRuns: 1 + diff --git a/apps/student/release_tests/flank_tablet_interaction_release.yml b/apps/student/release_tests/flank_tablet_interaction_release.yml new file mode 100644 index 0000000000..a17ea52dd0 --- /dev/null +++ b/apps/student/release_tests/flank_tablet_interaction_release.yml @@ -0,0 +1,29 @@ +gcloud: + project: delta-essence-114723 +# Use the next two lines to run locally +# app: ../build/outputs/apk/qa/debug/student-qa-debug.apk +# test: ../build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + app: ../apps/student/build/outputs/apk/qa/debug/student-qa-debug.apk + test: ../apps/student/build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + results-bucket: android-student + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - notAnnotation com.instructure.canvas.espresso.annotations.ReleaseExclude, com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubTablet, com.instructure.canvas.espresso.annotations.OfflineE2E + device: + - model: MediumTablet.arm + version: 29 + locale: en_US + orientation: landscape + - model: MediumTablet.arm + version: 29 + locale: en_US + orientation: portrait + +flank: + testShards: 10 + testRuns: 1 + diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/AnnouncementsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/AnnouncementsE2ETest.kt index eec078207e..9838344829 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/AnnouncementsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/AnnouncementsE2ETest.kt @@ -23,7 +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.canvas.espresso.utils.refresh import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.extensions.seedData diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ConferencesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ConferencesE2ETest.kt index bf8ed827c7..a0be1ffab3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ConferencesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ConferencesE2ETest.kt @@ -22,7 +22,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.canvas.espresso.utils.refresh import com.instructure.dataseeding.api.ConferencesApi import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.extensions.seedData diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DiscussionsE2ETest.kt index e82d90f94f..d2d2fa6497 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DiscussionsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DiscussionsE2ETest.kt @@ -26,7 +26,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.pressBackButton +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.dataseeding.api.FileFolderApi import com.instructure.dataseeding.api.FileUploadsApi diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt index b679ab3742..b9dab39d0d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt @@ -28,7 +28,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.pressBackButton +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.canvasapi2.managers.DiscussionManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.weave.awaitApiResponse diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt index 0878f955e8..e0f42a3526 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt @@ -24,6 +24,7 @@ import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.QuizzesApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.GradingType @@ -219,6 +220,103 @@ class GradesE2ETest: StudentComposeTest() { gradesPage.assertAllAssignmentItemCount(3) } + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.E2E) + fun testShowOnlyLetterGradeOnGradesPageE2E() { + 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(PREPARATION_TAG, "Grade submission: '${pointsTextAssignment.name}' with 12 points.") + SubmissionsApi.gradeSubmission(teacher.token, course.id, pointsTextAssignment.id, student.id, postedGrade = "12") + + Log.d(ASSERTION_TAG, "Assert that the grade is not displayed on the course's card by default.") + dashboardPage.assertCourseGradeNotDisplayed(course.name, "N/A", false) + + Log.d(STEP_TAG, "Toggle ON 'Show Grades' and navigate back to Dashboard Page.") + leftSideNavigationDrawerPage.setShowGrades(true) + + Log.d(ASSERTION_TAG, "Assert that the grade is displayed on the course's card.") + dashboardPage.assertCourseGrade(course.name, "N/A") + + Log.d(PREPARATION_TAG, "Update '${course.name}' course's settings: Enable restriction for quantitative data.") + val restrictQuantitativeDataMap = mutableMapOf() + restrictQuantitativeDataMap["restrict_quantitative_data"] = true + CoursesApi.updateCourseSettings(course.id, restrictQuantitativeDataMap) + + Log.d(ASSERTION_TAG, "Refresh the Dashboard page. Assert that the course grade is B-, as it is converted to letter grade because of the restriction.") + retryWithIncreasingDelay(times = 15, maxDelay = 5000) { + dashboardPage.refresh() + dashboardPage.assertCourseGrade(course.name, "B-") + } + + Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") + val percentageAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.PERCENT, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) + + Log.d(PREPARATION_TAG, "Grade submission: '${percentageAssignment.name}' with 66% of the maximum points (aka. 10).") + SubmissionsApi.gradeSubmission(teacher.token, course.id, percentageAssignment.id, student.id, postedGrade = "10") + + Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") + val letterGradeAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.LETTER_GRADE, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) + + Log.d(PREPARATION_TAG, "Grade submission: '${letterGradeAssignment.name}' with C.") + SubmissionsApi.gradeSubmission(teacher.token, course.id, letterGradeAssignment.id, student.id, postedGrade = "C") + + Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") + val passFailAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.PASS_FAIL, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) + + Log.d(PREPARATION_TAG, "Grade submission: '${passFailAssignment.name}' with 'Incomplete'.") + SubmissionsApi.gradeSubmission(teacher.token, course.id, passFailAssignment.id, student.id, postedGrade = "Incomplete") + + Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") + val gpaScaleAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.GPA_SCALE, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) + + Log.d(PREPARATION_TAG, "Grade submission: '${gpaScaleAssignment.name}' with 3.7.") + SubmissionsApi.gradeSubmission(teacher.token, course.id, gpaScaleAssignment.id, student.id, postedGrade = "3.7") + + Log.d(STEP_TAG, "Refresh the Dashboard page to let the newly added submissions and their grades propagate.") + dashboardPage.refresh() + + Log.d(STEP_TAG, "Select course: '${course.name}'. Select 'Grades' menu.") + dashboardPage.selectCourse(course) + courseBrowserPage.selectGrades() + + Log.d(ASSERTION_TAG, "Assert that the Total Grade is 'F' and all of the assignment grades are displayed properly (so they have been converted to letter grade).") + gradesPage.assertTotalGradeText("F") + gradesPage.assertAssignmentGradeText(pointsTextAssignment.name, "B-") + gradesPage.assertAssignmentGradeText(percentageAssignment.name, "D") + gradesPage.assertAssignmentGradeText(letterGradeAssignment.name, "C") + gradesPage.assertAssignmentGradeText(passFailAssignment.name, "Incomplete") + gradesPage.assertAssignmentGradeText(gpaScaleAssignment.name, "F") + + Log.d(PREPARATION_TAG, "Update '${course.name}' course's settings: Disable restriction for quantitative data.") + restrictQuantitativeDataMap["restrict_quantitative_data"] = false + CoursesApi.updateCourseSettings(course.id, restrictQuantitativeDataMap) + + Log.d(STEP_TAG, "Swipe to the top of the Course Grades Page and refresh it.") + gradesPage.scrollDownScreen() // First go to the top of the recycler view + gradesPage.refresh() // Actual refresh + + Log.d(ASSERTION_TAG, "Assert that the Total Grade is '49.47%' and all of the assignment grades are displayed properly. We now show numeric grades because restriction to quantitative data has been disabled.") + gradesPage.assertTotalGradeText("49.47%") + gradesPage.assertAssignmentGradeText(pointsTextAssignment.name, "12/15") + gradesPage.assertAssignmentGradeText(percentageAssignment.name, "66.67%") + gradesPage.assertAssignmentGradeText(letterGradeAssignment.name, "11.4/15 (C)") + gradesPage.assertAssignmentGradeText(passFailAssignment.name, "Incomplete") + gradesPage.scrollDownScreen() + gradesPage.assertAssignmentGradeText(gpaScaleAssignment.name, "3.7/15 (F)") + } + private fun makeQuizQuestions() = listOf( QuizQuestion( pointsPossible = 5, diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/HelpMenuE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/HelpMenuE2ETest.kt index 315780cb1a..22764d9e0b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/HelpMenuE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/HelpMenuE2ETest.kt @@ -25,7 +25,7 @@ import com.instructure.canvas.espresso.StringConstants.HelpMenu import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.utils.checkToastText import com.instructure.student.R import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.extensions.seedData @@ -77,7 +77,7 @@ class HelpMenuE2ETest : StudentTest() { try { helpPage.assertHelpMenuURL(HelpMenu.SEARCH_GUIDES_TITLE, "https://community.instructure.com/en/all-guides") - helpPage.assertHelpMenuURL(HelpMenu.SUBMIT_FEATURE_TITLE, "https://community.canvaslms.com/t5/Idea-Conversations/idb-p/ideas") + helpPage.assertHelpMenuURL(HelpMenu.SHARE_A_CONTRIBUTION_TITLE, "https://community.instructure.com/en/categories/product-connection") helpPage.assertHelpMenuURL(HelpMenu.SHARE_LOVE_TITLE, "market://details?id=com.instructure.candroid") } finally { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/LoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/LoginE2ETest.kt index d083081081..91487b9bc7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/LoginE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/LoginE2ETest.kt @@ -28,7 +28,7 @@ import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.annotations.Stub -import com.instructure.canvas.espresso.pressBackButton +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.EnrollmentsApi @@ -394,6 +394,7 @@ class LoginE2ETest : StudentTest() { loginSignInPage.assertPageObjects() } + @E2E @Test fun testFindSchoolPageObjects() { @@ -404,6 +405,7 @@ class LoginE2ETest : StudentTest() { loginFindSchoolPage.assertPageObjects() } + @E2E @Test fun testLoginLandingPageObjects() { @@ -411,6 +413,7 @@ class LoginE2ETest : StudentTest() { loginLandingPage.assertPageObjects() } + @E2E @Test fun testLoginSignInPageObjects() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/NotificationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/NotificationsE2ETest.kt index a275e5ade3..cfa4ea14db 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/NotificationsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/NotificationsE2ETest.kt @@ -17,15 +17,18 @@ package com.instructure.student.ui.e2e.classic import android.util.Log +import androidx.test.espresso.Espresso import androidx.test.espresso.NoMatchingViewException import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority +import com.instructure.canvas.espresso.SecondaryFeatureCategory 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.canvas.espresso.utils.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.ConversationsApi +import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.dataseeding.api.QuizzesApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.GradingType @@ -35,15 +38,20 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.espresso.getCustomDateCalendar +import com.instructure.espresso.retryWithIncreasingDelay +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.lang.Thread.sleep +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone @HiltAndroidTest -class NotificationsE2ETest : StudentTest() { +class NotificationsE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit @@ -143,6 +151,73 @@ class NotificationsE2ETest : StudentTest() { } } + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.NOTIFICATIONS, TestCategory.E2E, SecondaryFeatureCategory.DISCUSSION_CHECKPOINTS) + fun testDiscussionCheckpointsNotificationsE2E() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1, syllabusBody = "this is the syllabus body") + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + val discussionWithCheckpointsTitle = "Test Discussion with Checkpoints" + val assignmentName = "Test Assignment with Checkpoints" + + Log.d(PREPARATION_TAG, "Convert dates to match with different formats in different screens (Assignment Details)") + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + val assignmentDetailsDisplayFormat = SimpleDateFormat("MMM d, yyyy h:mm a", Locale.US) + val replyToTopicCalendar = getCustomDateCalendar(2) + val replyToEntryCalendar = getCustomDateCalendar(4) + val replyToTopicDueTime = dateFormat.format(replyToTopicCalendar.time) + val replyToEntryDueTime = dateFormat.format(replyToEntryCalendar.time) + val assignmentDetailsReplyToTopicDueDate = assignmentDetailsDisplayFormat.format(replyToTopicCalendar.time) + val assignmentDetailsReplyToEntryDueDate = assignmentDetailsDisplayFormat.format(replyToEntryCalendar.time) + + Log.d(PREPARATION_TAG, "Seed a discussion topic with checkpoints for '${course.name}' course.") + DiscussionTopicsApi.createDiscussionTopicWithCheckpoints(course.id, teacher.token, discussionWithCheckpointsTitle, assignmentName, replyToTopicDueTime, replyToEntryDueTime) + + Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") + tokenLogin(student) + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered.") + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Click on the 'Notifications' bottom menu to navigate to the Notifications list page.") + dashboardPage.clickNotificationsTab() + + Log.d(ASSERTION_TAG, "Assert that the notification about the discussion itself: '$discussionWithCheckpointsTitle' is displayed, and also the corresponding (parent) assignment: '$assignmentName' is displayed.") + retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { + refresh() }) + { + notificationPage.assertNotificationDisplayed(discussionWithCheckpointsTitle) + notificationPage.assertNotificationDisplayed("Assignment Created - $discussionWithCheckpointsTitle", contains = true) // Using contains logic since the assignment name alone is not the notification title, there are additional informations. + } + + Log.d(STEP_TAG, "Click on the notification about discussion: '${discussionWithCheckpointsTitle}'.") + notificationPage.clickNotification(discussionWithCheckpointsTitle) + + Log.d(ASSERTION_TAG, "Assert if the details webview page is displayed for '$discussionWithCheckpointsTitle' discussion.") + discussionDetailsPage.assertToolbarDiscussionTitle(discussionWithCheckpointsTitle) + + Log.d(STEP_TAG, "Navigate back to Notifications list page.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on the notification about the corresponding (parent) assignment: '${discussionWithCheckpointsTitle}'.") + notificationPage.clickNotification("Assignment Created - $discussionWithCheckpointsTitle", contains = true) + + Log.d(ASSERTION_TAG, "Assert that the Assignment Details Page is displayed properly with the correct toolbar title and subtitle.") + assignmentDetailsPage.assertDisplayToolbarTitle() + assignmentDetailsPage.assertDisplayToolbarSubtitle(course.name) + + Log.d(ASSERTION_TAG, "Assert that the checkpoints are displayed properly on the Assignment Details Page.") + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Reply to topic due", assignmentDetailsReplyToTopicDueDate) + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Additional replies (2) due", assignmentDetailsReplyToEntryDueDate) + } + private fun makeQuizQuestions() = listOf( QuizQuestion( questionText = "What's your favorite color?", diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/QuizzesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/QuizzesE2ETest.kt index c3599f92bc..adce215b1e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/QuizzesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/QuizzesE2ETest.kt @@ -29,7 +29,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.pressBackButton +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.dataseeding.api.QuizzesApi import com.instructure.dataseeding.model.QuizAnswer import com.instructure.dataseeding.model.QuizQuestion diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt index cb94725be3..c1d112aedf 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt @@ -25,7 +25,7 @@ import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.annotations.Stub -import com.instructure.canvas.espresso.pressBackButton +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAnnouncementsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAnnouncementsE2ETest.kt index adfa998225..16b1297796 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAnnouncementsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAnnouncementsE2ETest.kt @@ -27,7 +27,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.espresso.getVideoPosition import com.instructure.student.ui.utils.StudentComposeTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAssignmentsE2ETest.kt index 57c4832d51..0717d44007 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAssignmentsE2ETest.kt @@ -24,8 +24,8 @@ import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage -import com.instructure.canvas.espresso.pressBackButton -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.pressBackButton +import com.instructure.canvas.espresso.utils.refresh import com.instructure.dataseeding.api.AssignmentGroupsApi import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi @@ -34,6 +34,7 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin @@ -188,7 +189,10 @@ class OfflineAssignmentsE2ETest : StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert that the assignment '${gradedAssignment.name}' is displayed, " + "while '${notSubmittedAssignment.name}', '${submittedAssignment.name}', and '${otherTypeAssignment.name}' are not displayed on the assignment list.") - assignmentListPage.assertHasAssignment(gradedAssignment) + + retryWithIncreasingDelay(times = 25, maxDelay = 3000, catchBlock = { refresh() }) { // We need this retry block here because the grading status might not be updated to 'Graded' immediately as grading sometimes slow on beta. + assignmentListPage.assertHasAssignment(gradedAssignment) + } assignmentListPage.assertAssignmentNotDisplayed(notSubmittedAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(submittedAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(otherTypeAssignment.name) @@ -210,7 +214,7 @@ class OfflineAssignmentsE2ETest : StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert that the corresponding views are displayed on the Assignment Details Page, and there IS a submission for it. Navigate back to Assignment List Page.") assignmentDetailsPage.assertPageObjects() assignmentDetailsPage.assertStatusSubmitted() - assignmentDetailsPage.assertSubmissionAndRubricLabel() + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() assignmentDetailsPage.assertStatusSubmitted() Log.d(ASSERTION_TAG, "Assert that the (Re)submit Assignment button is not enabled as submitting assignments is not supported in offline mode.") @@ -242,8 +246,8 @@ class OfflineAssignmentsE2ETest : StudentComposeTest() { assignmentDetailsPage.assertPageObjects() assignmentDetailsPage.assertStatusNotSubmitted() - Log.d(ASSERTION_TAG, "Assert that 'Submission & Rubric' label is displayed.") - assignmentDetailsPage.assertSubmissionAndRubricLabel() + Log.d(ASSERTION_TAG, "Assert that 'Submission & Feedback' label is displayed.") + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() Log.d(STEP_TAG, "Navigate to Submission Details Page by clicking on the submission.") assignmentDetailsPage.goToSubmissionDetails() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDiscussionsE2ETest.kt index 0242c70521..f7e2dd8c4e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDiscussionsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDiscussionsE2ETest.kt @@ -25,9 +25,9 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E -import com.instructure.canvas.espresso.checkToastText -import com.instructure.canvas.espresso.pressBackButton -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.checkToastText +import com.instructure.canvas.espresso.utils.pressBackButton +import com.instructure.canvas.espresso.utils.refresh import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.dataseeding.api.FileFolderApi import com.instructure.dataseeding.api.FileUploadsApi @@ -309,6 +309,226 @@ class OfflineDiscussionsE2ETest : StudentComposeTest() { nativeDiscussionDetailsPage.assertTitleText(discussionWithCheckpointsTitle) } + @OfflineE2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.E2E, SecondaryFeatureCategory.DISCUSSION_CHECKPOINTS) + fun testOfflineDiscussionCheckpointsE2E() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1, syllabusBody = "Syllabus body") + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG, "Seed a discussion topic with checkpoints for '${course.name}' course.") + val discussionTitle = "Test Discussion with Checkpoints" + val assignmentName = "Test Assignment with Checkpoints" + val replyToTopicDueDate = "2029-11-12T22:59:00Z" + val replyToEntryDueDate = "2029-11-19T22:59:00Z" + DiscussionTopicsApi.createDiscussionTopicWithCheckpoints( + courseId = course.id, + token = teacher.token, + discussionTitle = discussionTitle, + assignmentName = assignmentName, + replyToTopicDueDate = replyToTopicDueDate, + replyToEntryDueDate = replyToEntryDueDate + ) + + val checkpointTopicDueDateForList = "Due " + convertIso8601ToCanvasFormat(replyToTopicDueDate) + " 2:59 PM" + val checkpointEntryDueDateForList = "Due " + convertIso8601ToCanvasFormat(replyToEntryDueDate) + " 2:59 PM" + val checkpointTopicDueDateForDetails = convertIso8601ToCanvasFormat(replyToTopicDueDate) + " 2:59 PM" + val checkpointEntryDueDateForDetails = convertIso8601ToCanvasFormat(replyToEntryDueDate) + " 2:59 PM" + + Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open the '${course.name}' course's 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.clickCourseOverflowMenu(course.name, "Manage Offline Content") + + Log.d(STEP_TAG, "Expand '${course.name}' course.") + manageOfflineContentPage.expandCollapseItem(course.name) + + Log.d(STEP_TAG, "Select 'Assignments', 'Discussions', 'Grades' and 'Syllabus' of '${course.name}' course for sync. Click on the 'Sync' button.") + manageOfflineContentPage.changeItemSelectionState("Assignments") + manageOfflineContentPage.changeItemSelectionState("Discussions") + manageOfflineContentPage.changeItemSelectionState("Grades") + manageOfflineContentPage.changeItemSelectionState("Syllabus") + manageOfflineContentPage.clickOnSyncButtonAndConfirm() + + Log.d(ASSERTION_TAG, "Assert that the offline sync icon only displayed on the synced course's course card.") + dashboardPage.assertCourseOfflineSyncIconVisible(course.name) + device.waitForIdle() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + turnOffConnectionViaADB() + OfflineTestUtils.waitForNetworkToGoOffline(device) + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.") + dashboardPage.waitForRender() + refresh() + + Log.d(ASSERTION_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.") + OfflineTestUtils.assertOfflineIndicator() + + Log.d(STEP_TAG, "Select course: '${course.name}' and navigate to Assignments Page.") + dashboardPage.selectCourse(course) + courseBrowserPage.selectAssignments() + + Log.d(ASSERTION_TAG, "Assert that the '$discussionTitle' discussion is present with 2 checkpoint due dates in the Assignment List.") + assignmentListPage.assertHasAssignmentWithCheckpoints(discussionTitle, dueAtString = checkpointTopicDueDateForList, dueAtStringSecondCheckpoint = checkpointEntryDueDateForList, expectedGrade = "-/15") + + Log.d(ASSERTION_TAG, "Assert that the checkpoint sub-items are displayed as separate assignment rows (visible because Syllabus was synced).") + assignmentListPage.assertHasAssignment("$discussionTitle Reply to Topic", replyToTopicDueDate, "-/10") + assignmentListPage.assertHasAssignment("$discussionTitle Required Replies (2)", replyToEntryDueDate, "-/5") + + Log.d(STEP_TAG, "Click on the expand icon for the '$discussionTitle' discussion to see the checkpoints' details.") + assignmentListPage.clickDiscussionCheckpointExpandCollapseIcon(discussionTitle) + + Log.d(ASSERTION_TAG, "Assert that the checkpoints' details are displayed correctly (titles, due dates, points possible, grades).") + assignmentListPage.assertDiscussionCheckpointDetails(2, dueAtReplyToTopic = checkpointTopicDueDateForList, dueAtAdditionalReplies = checkpointEntryDueDateForList, gradeReplyToTopic = "-/10", gradeAdditionalReplies = "-/5") + + Log.d(STEP_TAG, "Collapse the checkpoint details of the '$discussionTitle' discussion before navigating away.") + assignmentListPage.clickDiscussionCheckpointExpandCollapseIcon(discussionTitle) + + Log.d(STEP_TAG, "Click on '$discussionTitle Reply to Topic' checkpoint assignment to navigate to its Assignment Details Page.") + assignmentListPage.clickAssignment("$discussionTitle Reply to Topic") + + Log.d(ASSERTION_TAG, "Assert that the Assignment Details Page is displayed with the correct title for the 'Reply to Topic' checkpoint.") + assignmentDetailsPage.assertDisplayToolbarTitle() + assignmentDetailsPage.assertAssignmentTitle("$discussionTitle Reply to Topic") + + Log.d(ASSERTION_TAG, "Assert that the due date is '$checkpointTopicDueDateForDetails', the submission status is 'Not Submitted' and the submission type is 'Discussion Topic' for the 'Reply to Topic' checkpoint.") + assignmentDetailsPage.assertDisplaysDate(checkpointTopicDueDateForDetails) + assignmentDetailsPage.assertStatusNotSubmitted() + assignmentDetailsPage.assertSubmissionTypeDisplayed("Discussion Topic") + + Log.d(STEP_TAG, "Navigate back to the Assignment List Page.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on '$discussionTitle Required Replies (2)' checkpoint assignment to navigate to its Assignment Details Page.") + assignmentListPage.clickAssignment("$discussionTitle Required Replies (2)") + + Log.d(ASSERTION_TAG, "Assert that the Assignment Details Page is displayed with the correct title for the 'Required Replies (2)' checkpoint.") + assignmentDetailsPage.assertDisplayToolbarTitle() + assignmentDetailsPage.assertAssignmentTitle("$discussionTitle Required Replies (2)") + + Log.d(ASSERTION_TAG, "Assert that the due date is '$checkpointEntryDueDateForDetails', the submission status is 'Not Submitted' and the submission type is 'Discussion Topic' for the 'Required Replies (2)' checkpoint.") + assignmentDetailsPage.assertDisplaysDate(checkpointEntryDueDateForDetails) + assignmentDetailsPage.assertStatusNotSubmitted() + assignmentDetailsPage.assertSubmissionTypeDisplayed("Discussion Topic") + + Log.d(STEP_TAG, "Navigate back to the Assignment List Page.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on '$discussionTitle' assignment to navigate to the Assignment Details Page.") + assignmentListPage.clickAssignment(discussionTitle) + + Log.d(ASSERTION_TAG, "Assert that the Assignment Details Page is displayed properly with the correct toolbar title and subtitle.") + assignmentDetailsPage.assertDisplayToolbarTitle() + assignmentDetailsPage.assertAssignmentTitle(discussionTitle) + assignmentDetailsPage.assertDisplayToolbarSubtitle(course.name) + + Log.d(ASSERTION_TAG, "Assert that the checkpoint grades view is displayed correctly on the Assignment Details Page.") + assignmentDetailsPage.assertCheckpointGradesView("Reply to topic", "-/10") + assignmentDetailsPage.assertCheckpointGradesView("Additional replies (2)", "-/5") + + Log.d(ASSERTION_TAG, "Assert that the checkpoint due dates are displayed properly on the Assignment Details Page.") + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Reply to topic due", checkpointTopicDueDateForDetails) + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Additional replies (2) due", checkpointEntryDueDateForDetails) + + Log.d(ASSERTION_TAG, "Assert that the submission type is 'Discussion Topic' for the '$discussionTitle' assignment") + assignmentDetailsPage.assertSubmissionTypeDisplayed("Discussion Topic") + + Log.d(STEP_TAG, "Click on the 'View Discussion' button to navigate to the Discussion Details page from the Assignment Details page.") + assignmentDetailsPage.clickSubmit() + + Log.d(ASSERTION_TAG, "Assert that the Discussion Details page is displayed with the correct title.") + nativeDiscussionDetailsPage.assertTitleText(discussionTitle) + + Log.d(STEP_TAG, "Navigate back to the Assignment Details page.") + Espresso.pressBack() + + Log.d(ASSERTION_TAG, "Assert that we are back on the Assignment Details page.") + assignmentDetailsPage.assertAssignmentTitle(discussionTitle) + + Log.d(STEP_TAG, "Navigate back to the Course Browser Page.") + pressBackButton(2) + + Log.d(STEP_TAG, "Navigate to Discussions Page.") + courseBrowserPage.selectDiscussions() + + Log.d(ASSERTION_TAG, "Assert that the '$discussionTitle' discussion is displayed in the Discussion List.") + discussionListPage.assertTopicDisplayed(discussionTitle) + + Log.d(ASSERTION_TAG, "Assert that the checkpoint due dates are displayed for the '$discussionTitle' discussion in the Discussion List.") + discussionListPage.assertCheckpointDueDates(discussionTitle, checkpointTopicDueDateForList) + discussionListPage.assertCheckpointDueDates(discussionTitle, checkpointEntryDueDateForList) + + Log.d(ASSERTION_TAG, "Assert that the total points (15 pts), 0 replies and 0 unread replies are displayed for the '$discussionTitle' discussion in the Discussion List.") + discussionListPage.assertPointsDisplayed(discussionTitle, 15) + discussionListPage.assertReplyCount(discussionTitle, 0) + discussionListPage.assertUnreadReplyCount(discussionTitle, 0) + + Log.d(STEP_TAG, "Click on '$discussionTitle' discussion to navigate to the Discussion Details page.") + discussionListPage.selectTopic(discussionTitle) + + Log.d(ASSERTION_TAG, "Assert that the Discussion Details page is displayed with the correct title, 15 pts and no replies.") + nativeDiscussionDetailsPage.assertTitleText(discussionTitle) + nativeDiscussionDetailsPage.assertPointsPossibleDisplayed("15 pts") + nativeDiscussionDetailsPage.assertNoRepliesDisplayed() + + Log.d(STEP_TAG, "Navigate back to the Course Browser Page.") + pressBackButton(2) + + Log.d(STEP_TAG, "Navigate to Grades Page.") + courseBrowserPage.selectGrades() + + Log.d(ASSERTION_TAG, "Assert that the '$discussionTitle' main discussion and both checkpoint sub-assignments are displayed on the Grades Page.") + gradesPage.assertAssignmentIsDisplayed(discussionTitle) + gradesPage.assertAssignmentIsDisplayed("$discussionTitle Reply to Topic") + gradesPage.assertAssignmentIsDisplayed("$discussionTitle Required Replies (2)") + + Log.d(ASSERTION_TAG, "Assert that the grades (-/15, -/10, -/5) are displayed correctly for the discussion and its checkpoints on the Grades Page.") + gradesPage.assertAssignmentGradeText(discussionTitle, "-/15") + gradesPage.assertAssignmentGradeText("$discussionTitle Reply to Topic", "-/10") + gradesPage.assertAssignmentGradeText("$discussionTitle Required Replies (2)", "-/5") + + Log.d(ASSERTION_TAG, "Assert that the submission status is 'Not Submitted' for all three discussion assignments.") + gradesPage.assertAssignmentStatus(discussionTitle, "Not Submitted") + gradesPage.assertAssignmentStatus("$discussionTitle Reply to Topic", "Not Submitted") + gradesPage.assertAssignmentStatus("$discussionTitle Required Replies (2)", "Not Submitted") + + Log.d(STEP_TAG, "Navigate back to the Course Browser Page.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Navigate to Syllabus Page and the 'Summary' tab within it.") + courseBrowserPage.selectSyllabus() + syllabusPage.selectSummaryTab() + + Log.d(ASSERTION_TAG, "Assert that the discussion checkpoints are displayed as separate items in the Syllabus Summary.") + syllabusPage.assertItemDisplayed("$discussionTitle Reply to Topic") + syllabusPage.assertItemDisplayed("$discussionTitle Required Replies (2)") + + Log.d(STEP_TAG, "Select '$discussionTitle Reply to Topic' syllabus summary event.") + syllabusPage.selectSummaryEvent("$discussionTitle Reply to Topic") + + Log.d(ASSERTION_TAG, "Assert that the Assignment Details Page is displayed properly.") + assignmentDetailsPage.assertPageObjects() + assignmentDetailsPage.assertDisplayToolbarTitle() + + Log.d(ASSERTION_TAG, "Assert that the checkpoint grades view is displayed correctly on the Assignment Details Page.") + assignmentDetailsPage.assertCheckpointGradesView("Reply to topic", "-/10") + assignmentDetailsPage.assertCheckpointGradesView("Additional replies (2)", "-/5") + + Log.d(ASSERTION_TAG, "Assert that the checkpoint due dates are displayed properly on the Assignment Details Page.") + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Reply to topic due", checkpointTopicDueDateForDetails) + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Additional replies (2) due", checkpointEntryDueDateForDetails) + + Log.d(ASSERTION_TAG, "Assert that the submission type is 'Discussion Topic' for the '$discussionTitle' assignment") + assignmentDetailsPage.assertSubmissionTypeDisplayed("Discussion Topic") + } + @After fun tearDown() { Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device, so it will come back online.") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLeftSideMenuE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLeftSideMenuE2ETest.kt index c48435baa6..59e60f35cd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLeftSideMenuE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLeftSideMenuE2ETest.kt @@ -23,7 +23,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLoginE2ETest.kt index 9ac702bffd..4c9f1a65be 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLoginE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLoginE2ETest.kt @@ -23,7 +23,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.extensions.seedData diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt index 00754ec014..e4b60abc90 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt @@ -24,7 +24,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.dataseeding.api.ModulesApi diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePagesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePagesE2ETest.kt index 1d7dbfe403..1a21b749ec 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePagesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePagesE2ETest.kt @@ -26,7 +26,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.dataseeding.api.PagesApi import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePeopleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePeopleE2ETest.kt index 6d6eb60128..5972482033 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePeopleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePeopleE2ETest.kt @@ -24,7 +24,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSettingsE2ETest.kt index 2cab83b195..6b225a0f57 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSettingsE2ETest.kt @@ -23,7 +23,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyllabusE2ETest.kt index 8969573d0a..890f2b767f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyllabusE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyllabusE2ETest.kt @@ -23,7 +23,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncProgressE2ETest.kt index 0c21032139..07a3cf7d8b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncProgressE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncProgressE2ETest.kt @@ -26,7 +26,7 @@ import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.annotations.Stub -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncSettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncSettingsE2ETest.kt index ccbf589970..5881802cff 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncSettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncSettingsE2ETest.kt @@ -23,7 +23,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.OfflineE2E -import com.instructure.canvas.espresso.pressBackButton +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.pandautils.R import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/AssignmentsE2ETest.kt index 3d8408657c..883b0110e9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/AssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/AssignmentsE2ETest.kt @@ -28,13 +28,14 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.checkToastText import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage -import com.instructure.canvas.espresso.pressBackButton +import com.instructure.canvas.espresso.utils.checkToastText +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.dataseeding.api.AssignmentGroupsApi import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.FileUploadsApi +import com.instructure.dataseeding.api.GradingPeriodsApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.FileUploadType import com.instructure.dataseeding.model.GradingType @@ -109,8 +110,8 @@ class AssignmentsE2ETest: StudentComposeTest() { assignmentDetailsPage.assertPageObjects() assignmentDetailsPage.assertStatusNotSubmitted() - Log.d(ASSERTION_TAG, "Assert that 'Submission & Rubric' label is displayed and navigate to Submission Details Page.") - assignmentDetailsPage.assertSubmissionAndRubricLabel() + Log.d(ASSERTION_TAG, "Assert that 'Submission & Feedback' label is displayed and navigate to Submission Details Page.") + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() assignmentDetailsPage.goToSubmissionDetails() Log.d(ASSERTION_TAG, "Assert that there is no submission yet for the '${pointsTextAssignment.name}' assignment.") @@ -125,7 +126,7 @@ class AssignmentsE2ETest: StudentComposeTest() { Log.d(ASSERTION_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Feedback' label is displayed.") assignmentDetailsPage.refresh() assignmentDetailsPage.assertStatusSubmitted() - assignmentDetailsPage.assertSubmissionAndRubricLabel() + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() Log.d(PREPARATION_TAG, "Make another submission for assignment: '${pointsTextAssignment.name}' for student: '${student.name}'.") val secondSubmissionAttempt = SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY))) @@ -133,7 +134,7 @@ class AssignmentsE2ETest: StudentComposeTest() { Log.d(ASSERTION_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Feedback' label is displayed.") assignmentDetailsPage.refresh() assignmentDetailsPage.assertStatusSubmitted() - assignmentDetailsPage.assertSubmissionAndRubricLabel() + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() Log.d(ASSERTION_TAG, "Assert that the spinner is displayed and the last/newest attempt is selected.") assignmentDetailsPage.assertAttemptSpinnerDisplayed() @@ -542,8 +543,8 @@ class AssignmentsE2ETest: StudentComposeTest() { assignmentDetailsPage.assertPageObjects() assignmentDetailsPage.assertStatusNotSubmitted() - Log.d(ASSERTION_TAG, "Assert that 'Submission & Rubric' label is displayed.") - assignmentDetailsPage.assertSubmissionAndRubricLabel() + Log.d(ASSERTION_TAG, "Assert that 'Submission & Feedback' label is displayed.") + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() Log.d(STEP_TAG, "Navigate to Submission Details Page.") assignmentDetailsPage.goToSubmissionDetails() @@ -557,10 +558,10 @@ class AssignmentsE2ETest: StudentComposeTest() { Log.d(PREPARATION_TAG, "Submit assignment: '${pointsTextAssignment.name}' for student: '${student.name}'.") SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY))) - Log.d(ASSERTION_TAG, "Refresh the page. Assert that the assignment '${pointsTextAssignment.name}' has been submitted and the 'Submission & Rubric' label is displayed.") + Log.d(ASSERTION_TAG, "Refresh the page. Assert that the assignment '${pointsTextAssignment.name}' has been submitted and the 'Submission & Feedback' label is displayed.") assignmentDetailsPage.refresh() assignmentDetailsPage.assertStatusSubmitted() - assignmentDetailsPage.assertSubmissionAndRubricLabel() + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() Log.d(PREPARATION_TAG, "Grade submission: '${pointsTextAssignment.name}' with 13 points.") val textGrade = SubmissionsApi.gradeSubmission(teacher.token, course.id, pointsTextAssignment.id, student.id, postedGrade = "13") @@ -690,7 +691,7 @@ class AssignmentsE2ETest: StudentComposeTest() { @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.E2E) - fun testMultipleAssignmentsE2E() { + fun testMultipleAssignmentsWithSearchE2E() { Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(teachers = 1, courses = 1, students = 1) @@ -729,6 +730,36 @@ class AssignmentsE2ETest: StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert that '${letterGradeTextAssignment.name}' assignment is displayed with the corresponding grade: 16.") assignmentListPage.assertHasAssignment(letterGradeTextAssignment, "16") + + Log.d(STEP_TAG, "Click on the 'Search' (magnifying glass) icon at the toolbar.") + assignmentListPage.searchBar.clickOnSearchButton() + + Log.d(STEP_TAG, "Type the name of the '${letterGradeTextAssignment.name}' assignment into the search bar.") + assignmentListPage.searchBar.typeToSearchBar(letterGradeTextAssignment.name) + + Log.d(ASSERTION_TAG, "Assert that only '${letterGradeTextAssignment.name}' is displayed and '${pointsTextAssignment.name}' is not.") + assignmentListPage.assertHasAssignment(letterGradeTextAssignment) + assignmentListPage.assertAssignmentNotDisplayed(pointsTextAssignment.name) + + Log.d(STEP_TAG, "Clear the search input.") + assignmentListPage.searchBar.clickOnClearSearchButton() + + Log.d(ASSERTION_TAG, "Assert that both assignments are visible again after clearing the search.") + assignmentListPage.assertHasAssignment(pointsTextAssignment, "13") + assignmentListPage.assertHasAssignment(letterGradeTextAssignment, "16") + + Log.d(STEP_TAG, "Type a search query that does not match any assignment.") + assignmentListPage.searchBar.typeToSearchBar("xxxxxxxxxxx") + + Log.d(ASSERTION_TAG, "Assert that the empty state ('No Assignments') view is displayed when no assignments match the search query.") + assignmentListPage.assertDisplaysNoAssignmentsView() + + Log.d(STEP_TAG, "Close the search bar.") + assignmentListPage.searchBar.pressSearchBarButton() + + Log.d(ASSERTION_TAG, "Assert that both assignments are displayed again after closing the search bar.") + assignmentListPage.assertHasAssignment(pointsTextAssignment, "13") + assignmentListPage.assertHasAssignment(letterGradeTextAssignment, "16") } @E2E @@ -741,13 +772,13 @@ class AssignmentsE2ETest: StudentComposeTest() { val teacher = data.teachersList[0] val course = data.coursesList[0] - Log.d(PREPARATION_TAG, "Seeding assignment for '${course.name}' course.") - val upcomingAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.LETTER_GRADE, pointsPossible = 20.0, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) + Log.d(PREPARATION_TAG, "Seeding an upcoming assignment (future due date) for '${course.name}' course.") + val upcomingAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.LETTER_GRADE, pointsPossible = 20.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) - Log.d(PREPARATION_TAG, "Seeding assignment for '${course.name}' course.") + Log.d(PREPARATION_TAG, "Seeding an overdue assignment (past due date) for '${course.name}' course.") val missingAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.LETTER_GRADE, pointsPossible = 20.0, dueAt = 2.days.ago.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) - Log.d(PREPARATION_TAG, "Seeding a GRADED assignment for '${course.name}' course.") + Log.d(PREPARATION_TAG, "Seeding a GRADED assignment (no due date) for '${course.name}' course.") val gradedAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.LETTER_GRADE, pointsPossible = 20.0, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) Log.d(PREPARATION_TAG, "Grade the '${gradedAssignment.name}' with '11' points out of 20.") @@ -756,7 +787,7 @@ class AssignmentsE2ETest: StudentComposeTest() { Log.d(PREPARATION_TAG, "Create an Assignment Group for '${course.name}' course.") val assignmentGroup = AssignmentGroupsApi.createAssignmentGroup(teacher.token, course.id, name = "Discussions") - Log.d(PREPARATION_TAG, "Seeding assignment for '${course.name}' course.") + Log.d(PREPARATION_TAG, "Seeding an assignment in the 'Discussions' group (no due date) for '${course.name}' course.") val otherTypeAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.LETTER_GRADE, pointsPossible = 20.0, assignmentGroupId = assignmentGroup.id, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") @@ -773,22 +804,22 @@ class AssignmentsE2ETest: StudentComposeTest() { assignmentListPage.assertHasAssignment(otherTypeAssignment) assignmentListPage.assertHasAssignment(gradedAssignment) - Log.d(STEP_TAG, "Filter the 'Not Yet Submitted' assignments.") + Log.d(STEP_TAG, "Filter to show only 'Not Yet Submitted' assignments.") assignmentListPage.filterAssignments("Assignment Filter", AssignmentListPage.FilterOption.ToBeGraded) assignmentListPage.filterAssignments("Assignment Filter", AssignmentListPage.FilterOption.Graded) assignmentListPage.filterAssignments("Assignment Filter", AssignmentListPage.FilterOption.Other) - Log.d(ASSERTION_TAG, "Assert that the '${missingAssignment.name}' 'Not Yet Submitted' assignment is displayed and the others at NOT.") + Log.d(ASSERTION_TAG, "Assert that only the '${missingAssignment.name}' 'Not Yet Submitted' assignment is displayed and the others are NOT.") assignmentListPage.assertHasAssignment(missingAssignment) assignmentListPage.assertAssignmentNotDisplayed(upcomingAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(otherTypeAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(gradedAssignment.name) - Log.d(STEP_TAG, "Filter the 'GRADED' assignments.") + Log.d(STEP_TAG, "Filter to show only 'Graded' assignments.") assignmentListPage.filterAssignments("Assignment Filter", AssignmentListPage.FilterOption.NotYetSubmitted) assignmentListPage.filterAssignments("Assignment Filter", AssignmentListPage.FilterOption.Graded) - Log.d(ASSERTION_TAG, "Assert that the '${gradedAssignment.name}' GRADED assignment is displayed.") + Log.d(ASSERTION_TAG, "Assert that only the '${gradedAssignment.name}' 'Graded' assignment is displayed and the others are NOT.") assignmentListPage.assertHasAssignment(gradedAssignment) assignmentListPage.assertAssignmentNotDisplayed(upcomingAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(otherTypeAssignment.name) @@ -799,8 +830,10 @@ class AssignmentsE2ETest: StudentComposeTest() { assignmentListPage.filterAssignments("Assignment Filter", AssignmentListPage.FilterOption.ToBeGraded) assignmentListPage.filterAssignments("Assignment Filter", AssignmentListPage.FilterOption.Other) - Log.d(ASSERTION_TAG, "Assert that still all the assignment are displayed and the corresponding groups (Assignments, Discussions) as well.") + Log.d(STEP_TAG, "Group assignments by 'Assignment Group'.") assignmentListPage.groupByAssignments(AssignmentListPage.GroupByOption.AssignmentGroup) + + Log.d(ASSERTION_TAG, "Assert that the 'Assignments' and 'Discussions' groups are displayed with all assignments inside.") assignmentListPage.assertAssignmentGroupDisplayed("Assignments") assignmentListPage.assertAssignmentGroupDisplayed("Discussions") assignmentListPage.assertHasAssignment(upcomingAssignment) @@ -811,31 +844,120 @@ class AssignmentsE2ETest: StudentComposeTest() { Log.d(STEP_TAG, "Collapse the 'Assignments' assignment group.") assignmentListPage.expandCollapseAssignmentGroup("Assignments") - Log.d(ASSERTION_TAG, "Assert that it's items are not displayed when the group is collapsed.") + Log.d(ASSERTION_TAG, "Assert that the items inside 'Assignments' group are NOT displayed when collapsed.") assignmentListPage.assertAssignmentNotDisplayed(upcomingAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(missingAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(gradedAssignment.name) - Log.d(ASSERTION_TAG, "Assert that the other group's item is still displayed.") + Log.d(ASSERTION_TAG, "Assert that the 'Discussions' group item is still displayed.") assignmentListPage.assertHasAssignment(otherTypeAssignment) - Log.d(STEP_TAG, "Expand the 'Assignments' assignment group.") + Log.d(STEP_TAG, "Expand the 'Assignments' group back.") assignmentListPage.expandCollapseAssignmentGroup("Assignments") - Log.d(STEP_TAG, "Click on the 'Search' (magnifying glass) icon at the toolbar.") - assignmentListPage.searchBar.clickOnSearchButton() + Log.d(ASSERTION_TAG, "Assert that all assignments are visible again after expanding.") + assignmentListPage.assertHasAssignment(upcomingAssignment) + assignmentListPage.assertHasAssignment(missingAssignment) + assignmentListPage.assertHasAssignment(gradedAssignment) + + Log.d(STEP_TAG, "Group assignments by 'Due Date'.") + assignmentListPage.groupByAssignments(AssignmentListPage.GroupByOption.DueDate) + + Log.d(ASSERTION_TAG, "Assert that the three due date groups are displayed.") + assignmentListPage.assertAssignmentGroupDisplayed("Overdue Assignments") + assignmentListPage.assertAssignmentGroupDisplayed("Upcoming Assignments") + assignmentListPage.assertAssignmentGroupDisplayed("Undated Assignments") + + Log.d(ASSERTION_TAG, "Assert that all assignments are visible in the correct due date groups: '${missingAssignment.name}' overdue, '${upcomingAssignment.name}' upcoming, '${gradedAssignment.name}' and '${otherTypeAssignment.name}' undated.") + assignmentListPage.assertHasAssignment(missingAssignment) + assignmentListPage.assertHasAssignment(upcomingAssignment) + assignmentListPage.assertHasAssignment(gradedAssignment) + assignmentListPage.assertHasAssignment(otherTypeAssignment) + + Log.d(STEP_TAG, "Collapse the 'Overdue Assignments' group.") + assignmentListPage.expandCollapseAssignmentGroup("Overdue Assignments") + + Log.d(ASSERTION_TAG, "Assert that the '${missingAssignment.name}' overdue assignment is NOT displayed when the group is collapsed.") + assignmentListPage.assertAssignmentNotDisplayed(missingAssignment.name) + + Log.d(ASSERTION_TAG, "Assert that assignments in the other groups are still displayed.") + assignmentListPage.assertHasAssignment(upcomingAssignment) + assignmentListPage.assertHasAssignment(gradedAssignment) + assignmentListPage.assertHasAssignment(otherTypeAssignment) - Log.d(STEP_TAG, "Type the name of the '${missingAssignment.name}' assignment.") - assignmentListPage.searchBar.typeToSearchBar(missingAssignment.name.drop(5)) + Log.d(STEP_TAG, "Expand the 'Overdue Assignments' group back.") + assignmentListPage.expandCollapseAssignmentGroup("Overdue Assignments") - Log.d(ASSERTION_TAG, "Assert that the '${missingAssignment.name}' assignment has been found by previously typed search string.") - sleep(3000) // Allow the search input to propagate + Log.d(ASSERTION_TAG, "Assert that the '${missingAssignment.name}' overdue assignment is visible again.") assignmentListPage.assertHasAssignment(missingAssignment) + + Log.d(STEP_TAG, "Collapse all three due date groups.") + assignmentListPage.expandCollapseAssignmentGroup("Overdue Assignments") + assignmentListPage.expandCollapseAssignmentGroup("Upcoming Assignments") + assignmentListPage.expandCollapseAssignmentGroup("Undated Assignments") + + Log.d(ASSERTION_TAG, "Assert that none of the assignments is displayed when all groups are collapsed.") + assignmentListPage.assertAssignmentNotDisplayed(missingAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(upcomingAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(otherTypeAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(gradedAssignment.name) } + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.E2E) + fun testGradingPeriodFilterE2E() { + Log.d(PREPARATION_TAG, "Seeding data with a grading period enabled.") + val data = seedData(teachers = 1, courses = 1, students = 1, gradingPeriods = true) + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG, "Fetching the grading period for '${course.name}' course.") + val gradingPeriod = GradingPeriodsApi.getGradingPeriodsOfCourse(course.id).gradingPeriods[0] + + Log.d(PREPARATION_TAG, "Seeding an assignment due within the grading period (2 days from now) for '${course.name}' course.") + val assignmentInPeriod = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 10.0, dueAt = 2.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) + + Log.d(PREPARATION_TAG, "Seeding an assignment due outside the grading period (30 days ago) for '${course.name}' course.") + val assignmentOutsidePeriod = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 10.0, dueAt = 30.days.ago.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.name}' course and navigate to its Assignments Page.") + dashboardPage.selectCourse(course) + courseBrowserPage.selectAssignments() + + Log.d(ASSERTION_TAG, "Assert that the grading period header shows '${gradingPeriod.title}' as the current grading period is pre-selected by default.") + assignmentListPage.assertGradingPeriodLabel(gradingPeriod.title) + + Log.d(ASSERTION_TAG, "Assert that only '${assignmentInPeriod.name}' is displayed by default (within the grading period) and '${assignmentOutsidePeriod.name}' is NOT.") + assignmentListPage.assertHasAssignment(assignmentInPeriod) + assignmentListPage.assertAssignmentNotDisplayed(assignmentOutsidePeriod.name) + + Log.d(STEP_TAG, "Switch the grading period filter to 'All Grading Periods'.") + assignmentListPage.filterAssignments("Grading Period", AssignmentListPage.FilterOption.GradingPeriod("All Grading Periods")) + + Log.d(ASSERTION_TAG, "Assert that the grading period header now shows 'All'.") + assignmentListPage.assertGradingPeriodLabel() + + Log.d(ASSERTION_TAG, "Assert that both assignments are visible when 'All Grading Periods' is selected.") + assignmentListPage.assertHasAssignment(assignmentInPeriod) + assignmentListPage.assertHasAssignment(assignmentOutsidePeriod) + + Log.d(STEP_TAG, "Switch back to the '${gradingPeriod.title}' grading period.") + assignmentListPage.filterAssignments("Grading Period", AssignmentListPage.FilterOption.GradingPeriod(gradingPeriod.title)) + + Log.d(ASSERTION_TAG, "Assert that the grading period header shows '${gradingPeriod.title}' again.") + assignmentListPage.assertGradingPeriodLabel(gradingPeriod.title) + + Log.d(ASSERTION_TAG, "Assert that only '${assignmentInPeriod.name}' is displayed again and '${assignmentOutsidePeriod.name}' is NOT.") + assignmentListPage.assertHasAssignment(assignmentInPeriod) + assignmentListPage.assertAssignmentNotDisplayed(assignmentOutsidePeriod.name) + } + @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.COMMENTS, TestCategory.E2E) @@ -1112,8 +1234,8 @@ class AssignmentsE2ETest: StudentComposeTest() { 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(ASSERTION_TAG, "Assert that 'Submission & Feedback' label is displayed and navigate to Submission Details Page.") + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() Log.d(STEP_TAG, "Click on the 'Submit Assignment' button.") assignmentDetailsPage.clickSubmit() @@ -1124,8 +1246,8 @@ class AssignmentsE2ETest: StudentComposeTest() { 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(ASSERTION_TAG, "Assert that 'Submission & Feedback' label is displayed and navigate to Submission Details Page.") + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() Log.d(STEP_TAG, "Click on the 'Submit Assignment' button.") assignmentDetailsPage.clickSubmit() @@ -1453,101 +1575,4 @@ class AssignmentsE2ETest: StudentComposeTest() { "Video position did not change. First: $firstVideoPositionText, Second: $secondVideoPositionText" } } - - @E2E - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.E2E) - fun testShowOnlyLetterGradeOnGradesPageE2E() { - 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(PREPARATION_TAG, "Grade submission: '${pointsTextAssignment.name}' with 12 points.") - SubmissionsApi.gradeSubmission(teacher.token, course.id, pointsTextAssignment.id, student.id, postedGrade = "12") - - Log.d(ASSERTION_TAG, "Assert that the grade is not displayed on the course's card by default.") - dashboardPage.assertCourseGradeNotDisplayed(course.name, "N/A", false) - - Log.d(STEP_TAG, "Toggle ON 'Show Grades' and navigate back to Dashboard Page.") - leftSideNavigationDrawerPage.setShowGrades(true) - - Log.d(ASSERTION_TAG, "Assert that the grade is displayed on the course's card.") - dashboardPage.assertCourseGrade(course.name, "N/A") - - Log.d(PREPARATION_TAG, "Update '${course.name}' course's settings: Enable restriction for quantitative data.") - val restrictQuantitativeDataMap = mutableMapOf() - restrictQuantitativeDataMap["restrict_quantitative_data"] = true - CoursesApi.updateCourseSettings(course.id, restrictQuantitativeDataMap) - - Log.d(ASSERTION_TAG, "Refresh the Dashboard page. Assert that the course grade is B-, as it is converted to letter grade because of the restriction.") - retryWithIncreasingDelay(times = 15, maxDelay = 5000) { - dashboardPage.refresh() - dashboardPage.assertCourseGrade(course.name, "B-") - } - - Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") - val percentageAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.PERCENT, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) - - Log.d(PREPARATION_TAG, "Grade submission: '${percentageAssignment.name}' with 66% of the maximum points (aka. 10).") - SubmissionsApi.gradeSubmission(teacher.token, course.id, percentageAssignment.id, student.id, postedGrade = "10") - - Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") - val letterGradeAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.LETTER_GRADE, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) - - Log.d(PREPARATION_TAG, "Grade submission: '${letterGradeAssignment.name}' with C.") - SubmissionsApi.gradeSubmission(teacher.token, course.id, letterGradeAssignment.id, student.id, postedGrade = "C") - - Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") - val passFailAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.PASS_FAIL, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) - - Log.d(PREPARATION_TAG, "Grade submission: '${passFailAssignment.name}' with 'Incomplete'.") - SubmissionsApi.gradeSubmission(teacher.token, course.id, passFailAssignment.id, student.id, postedGrade = "Incomplete") - - Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") - val gpaScaleAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.GPA_SCALE, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) - - Log.d(PREPARATION_TAG, "Grade submission: '${gpaScaleAssignment.name}' with 3.7.") - SubmissionsApi.gradeSubmission(teacher.token, course.id, gpaScaleAssignment.id, student.id, postedGrade = "3.7") - - Log.d(STEP_TAG, "Refresh the Dashboard page to let the newly added submissions and their grades propagate.") - dashboardPage.refresh() - - Log.d(STEP_TAG, "Select course: '${course.name}'. Select 'Grades' menu.") - dashboardPage.selectCourse(course) - courseBrowserPage.selectGrades() - - Log.d(ASSERTION_TAG, "Assert that the Total Grade is 'F' and all of the assignment grades are displayed properly (so they have been converted to letter grade).") - gradesPage.assertTotalGradeText("F") - gradesPage.assertAssignmentGradeText(pointsTextAssignment.name, "B-") - gradesPage.assertAssignmentGradeText(percentageAssignment.name, "D") - gradesPage.assertAssignmentGradeText(letterGradeAssignment.name, "C") - gradesPage.assertAssignmentGradeText(passFailAssignment.name, "Incomplete") - gradesPage.assertAssignmentGradeText(gpaScaleAssignment.name, "F") - - Log.d(PREPARATION_TAG, "Update '${course.name}' course's settings: Enable restriction for quantitative data.") - restrictQuantitativeDataMap["restrict_quantitative_data"] = false - CoursesApi.updateCourseSettings(course.id, restrictQuantitativeDataMap) - - Log.d(STEP_TAG, "Swipe to the top of the Course Grades Page and refresh it.") - gradesPage.scrollDownScreen() // First go to the top of the recycler view - gradesPage.refresh() // Actual refresh - - Log.d(ASSERTION_TAG, "Assert that the Total Grade is '49.47%' and all of the assignment grades are displayed properly. We now show numeric grades because restriction to quantitative data has been disabled.") - gradesPage.assertTotalGradeText("49.47%") - gradesPage.assertAssignmentGradeText(pointsTextAssignment.name, "12/15") - gradesPage.assertAssignmentGradeText(percentageAssignment.name, "66.67%") - gradesPage.assertAssignmentGradeText(letterGradeAssignment.name, "11.4/15 (C)") - gradesPage.assertAssignmentGradeText(passFailAssignment.name, "Incomplete") - gradesPage.scrollDownScreen() - gradesPage.assertAssignmentGradeText(gpaScaleAssignment.name, "3.7/15 (F)") - } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt index 29a4d9207d..527eca415e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt @@ -25,7 +25,8 @@ 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.pressBackButton +import com.instructure.canvas.espresso.utils.pressBackButton +import com.instructure.canvas.espresso.utils.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.PagesApi @@ -35,6 +36,7 @@ import com.instructure.dataseeding.model.UpdateCourse import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData @@ -282,11 +284,16 @@ class BookmarksE2ETest : StudentComposeTest() { Log.d(STEP_TAG, "Navigate to 'Notifications' page.") dashboardPage.clickNotificationsTab() - Log.d(ASSERTION_TAG, "Assert that the notification about assignment: '${assignment.name}' is displayed.") - notificationPage.assertNotificationDisplayed(assignment.name) + Log.d(ASSERTION_TAG, "Assert that the notification about the discussion itself: '${assignment.name}' is displayed.") + retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { + refresh() }) + { + Log.d(ASSERTION_TAG, "Assert that the notification about assignment: '${assignment.name}' is displayed.") + notificationPage.assertNotificationDisplayed(assignment.name, contains = true) + } Log.d(STEP_TAG, "Click on the notification about assignment: '${assignment.name}'.") - notificationPage.clickNotification(assignment.name) + notificationPage.clickNotification(assignment.name, contains = true) val notificationBookmarkName = "Second Bookmark" Log.d(STEP_TAG, "Add a new bookmark with name: '$notificationBookmarkName' from the assignment details page.") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt index 3413e11d85..048daa7474 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt @@ -23,7 +23,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.utils.checkToastText import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.espresso.getDateInCanvasCalendarFormat diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CustomStatusesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CustomStatusesE2ETest.kt index 4872bca225..2a23e1c0b4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CustomStatusesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CustomStatusesE2ETest.kt @@ -83,7 +83,7 @@ class CustomStatusesE2ETest: StudentComposeTest() { assignmentListPage.clickAssignment(testAssignment) assignmentDetailsPage.assertCustomStatus("AMAZING") - assignmentDetailsPage.assertSubmissionAndRubricLabel() + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() } @After diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/InboxE2ETest.kt index 1f9626a536..275a562e51 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/InboxE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/InboxE2ETest.kt @@ -31,7 +31,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.canvas.espresso.utils.refresh import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi import com.instructure.espresso.getVideoPosition diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/PeopleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/PeopleE2ETest.kt index a08306675b..a22f0be4f8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/PeopleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/PeopleE2ETest.kt @@ -22,7 +22,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.pressBackButton +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/RubricE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/RubricE2ETest.kt new file mode 100644 index 0000000000..85f4dd3e63 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/RubricE2ETest.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2026 - 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.student.ui.e2e.compose + +import android.util.Log +import androidx.test.espresso.Espresso +import com.instructure.canvas.espresso.FeatureCategory +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.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.api.SubmissionsApi +import com.instructure.dataseeding.model.GradingType +import com.instructure.dataseeding.model.RubricAssessmentEntry +import com.instructure.dataseeding.model.RubricCriterion +import com.instructure.dataseeding.model.RubricCriterionRating +import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.util.days +import com.instructure.dataseeding.util.fromNow +import com.instructure.dataseeding.util.iso8601 +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.seedAssignmentSubmission +import com.instructure.student.ui.utils.extensions.seedAssignmentWithRubric +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Test + +@HiltAndroidTest +class RubricE2ETest : StudentComposeTest() { + + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.RUBRICS, TestCategory.E2E) + fun testRubricDisplayedInSubmissionDetailsE2E() { + + 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 with 10 max points.") + val assignment = AssignmentsApi.createAssignment( + courseId = course.id, + teacherToken = teacher.token, + gradingType = GradingType.POINTS, + pointsPossible = 10.0, + dueAt = 1.days.fromNow.iso8601, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY) + ) + + val writingQualityCriterion = RubricCriterion( + description = "Writing Quality", + longDescription = "Evaluates the overall quality of written expression and clarity.", + points = 10.0, + ratings = listOf( + RubricCriterionRating(description = "Excellent", longDescription = "Demonstrates outstanding writing skills with clear and compelling expression.", points = 10.0), + RubricCriterionRating(description = "Satisfactory", longDescription = "Meets basic writing requirements with adequate clarity.", points = 5.0), + RubricCriterionRating(description = "Poor", longDescription = "Does not meet writing standards; lacks clarity and structure.", points = 0.0) + ) + ) + + val researchDepthCriterion = RubricCriterion( + description = "Research Depth", + longDescription = null, + points = 9.0, + ratings = listOf( + RubricCriterionRating(description = "Exceptional", longDescription = "Thorough and comprehensive research with strong source diversity.", points = 9.0), + RubricCriterionRating(description = "Proficient", longDescription = "Well-researched with only minor gaps in coverage.", points = 7.0), + RubricCriterionRating(description = "Developing", longDescription = "Adequate research coverage but missing important perspectives.", points = 4.0), + RubricCriterionRating(description = "Beginning", longDescription = "Limited research depth with significant gaps.", points = 2.0), + RubricCriterionRating(description = "Not good", longDescription = "Insufficient research; sources are missing or unreliable.", points = 1.0) + ) + ) + + Log.d(PREPARATION_TAG, "Creating a rubric with 2 criteria and associating it with '${assignment.name}' assignment.") + val rubric = seedAssignmentWithRubric( + courseId = course.id, + assignmentId = assignment.id, + teacherToken = teacher.token, + title = "Test Rubric", + criteria = listOf(writingQualityCriterion, researchDepthCriterion) + ) + + Log.d(PREPARATION_TAG, "Seeding a submission for '${assignment.name}' assignment with '${student.name}' student.") + seedAssignmentSubmission( + submissionSeeds = listOf(SubmissionsApi.SubmissionSeedInfo( + amount = 1, + submissionType = SubmissionType.ONLINE_TEXT_ENTRY + )), + assignmentId = assignment.id, + courseId = course.id, + studentToken = student.token + ) + + 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(STEP_TAG, "Click on assignment '${assignment.name}'.") + assignmentListPage.clickAssignment(assignment) + + Log.d(ASSERTION_TAG, "Assert that 'Submission & Feedback' label is displayed on the Assignment Details Page.") + assignmentDetailsPage.assertSubmissionAndFeedbackLabel() + + Log.d(STEP_TAG, "Navigate to Submission Details Page.") + assignmentDetailsPage.goToSubmissionDetails() + + Log.d(STEP_TAG, "Open the 'Rubric' tab in Submission Details.") + submissionDetailsPage.openRubric() + + Log.d(STEP_TAG, "Expand the sliding panel to see all of the Rubric criteria.") + submissionDetailsPage.expandSlidingPanel() + + Log.d(ASSERTION_TAG, "Assert that the 'Writing Quality' criterion description button is displayed and opens the long description.") + submissionDetailsPage.assertRubricDescriptionDisplays(writingQualityCriterion) + + Log.d(ASSERTION_TAG, "Assert that the 'Writing Quality' rubric criterion is displayed with all 3 ratings and their descriptions.") + submissionDetailsPage.assertRubricCriterionDisplayed(writingQualityCriterion) + + Log.d(ASSERTION_TAG, "Assert that the 'Research Depth' rubric criterion is displayed with all 5 ratings and their descriptions.") + submissionDetailsPage.assertRubricCriterionDisplayed(researchDepthCriterion) + + Log.d(STEP_TAG, "Grade the submission with rubric via API: selecting 'Poor' (defined rating, 0 pts) for 'Writing Quality' and a custom score of 3 pts for 'Research Depth'.") + val writingQualityCriterionResponse = rubric.criteria.first { it.description == writingQualityCriterion.description } + val poorRating = writingQualityCriterionResponse.ratings.first { it.description == "Poor" } + val researchDepthCriterionResponse = rubric.criteria.first { it.description == researchDepthCriterion.description } + SubmissionsApi.gradeSubmissionWithRubric( + teacherToken = teacher.token, + courseId = course.id, + assignmentId = assignment.id, + studentId = student.id, + rubricAssessment = mapOf( + writingQualityCriterionResponse.id to RubricAssessmentEntry(points = 0.0, ratingId = poorRating.id), + researchDepthCriterionResponse.id to RubricAssessmentEntry(points = 3.0) + ) + ) + + Log.d(STEP_TAG, "Navigate back to Assignment Details Page and refresh to pick up the new grade.") + Espresso.pressBack() + assignmentDetailsPage.refresh() + + Log.d(STEP_TAG, "Navigate to Submission Details Page.") + assignmentDetailsPage.goToSubmissionDetails() + + Log.d(STEP_TAG, "Open the 'Rubric' tab and expand the sliding panel.") + submissionDetailsPage.openRubric() + submissionDetailsPage.expandSlidingPanel() + + val writingQualityPoorRating = writingQualityCriterion.ratings.first { it.description == "Poor" } + val writingQualitySatisfactoryRating = writingQualityCriterion.ratings.first { it.description == "Satisfactory" } + + Log.d(ASSERTION_TAG, "Assert that the 'Writing Quality' 'Poor' rating is pre-selected as a defined rubric grade.") + submissionDetailsPage.assertRubricRatingSelected(writingQualityCriterion, writingQualityPoorRating) + + Log.d(ASSERTION_TAG, "Assert that 'Research Depth' shows a custom score as pre-selected.") + submissionDetailsPage.assertRubricCustomScoreSelected(researchDepthCriterion) + + Log.d(ASSERTION_TAG, "Assert that the 'Poor' rating is the actually assessed/graded rating (filled indicator).") + submissionDetailsPage.assertRubricRatingIsAssessed(writingQualityCriterion, writingQualityPoorRating) + + Log.d(STEP_TAG, "Tap on the 'Satisfactory' rating to preview its description without changing the actual grade.") + submissionDetailsPage.clickRubricRating(writingQualityCriterion, writingQualitySatisfactoryRating) + + Log.d(ASSERTION_TAG, "Assert that 'Satisfactory' description is shown after tapping it.") + submissionDetailsPage.assertRubricRatingSelected(writingQualityCriterion, writingQualitySatisfactoryRating) + + Log.d(ASSERTION_TAG, "Assert that 'Poor' is still the actual graded rating (filled indicator) even though 'Satisfactory' was tapped.") + submissionDetailsPage.assertRubricRatingIsAssessed(writingQualityCriterion, writingQualityPoorRating) + + Log.d(ASSERTION_TAG, "Assert that 'Satisfactory' is only preview selected (outlined indicator), not actually graded.") + submissionDetailsPage.assertRubricRatingIsPreviewSelected(writingQualityCriterion, writingQualitySatisfactoryRating) + } + +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/SettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/SettingsE2ETest.kt index c15553f422..d3fc026873 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/SettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/SettingsE2ETest.kt @@ -22,14 +22,14 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.IntentActionMatcher import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.checkToastText -import com.instructure.canvas.espresso.pressBackButton +import com.instructure.canvas.espresso.utils.IntentActionMatcher +import com.instructure.canvas.espresso.utils.checkToastText +import com.instructure.canvas.espresso.utils.pressBackButton import com.instructure.canvasapi2.utils.RemoteConfigParam import com.instructure.canvasapi2.utils.RemoteConfigUtils import com.instructure.dataseeding.api.ConversationsApi diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt index 59f5dd620d..7bf626ea38 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -37,14 +37,14 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.checkToastText import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvas.espresso.mockcanvas.addAssignment import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager import com.instructure.canvas.espresso.mockcanvas.init -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.checkToastText +import com.instructure.canvas.espresso.utils.refresh import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -668,10 +668,10 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentListPage.clickAssignment(assignment) assignmentDetailsPage.clickSubmit() - assignmentDetailsPage.assertSubmissionTypeDisplayed("Text Entry") - assignmentDetailsPage.assertSubmissionTypeDisplayed("Website URL") - assignmentDetailsPage.assertSubmissionTypeDisplayed("File Upload") - assignmentDetailsPage.assertSubmissionTypeDisplayed("Media Recording") + assignmentDetailsPage.assertSubmissionOptionDisplayed("Text Entry") + assignmentDetailsPage.assertSubmissionOptionDisplayed("Website URL") + assignmentDetailsPage.assertSubmissionOptionDisplayed("File Upload") + assignmentDetailsPage.assertSubmissionOptionDisplayed("Media Recording") //Try 1 submission to check if it's possible to submit even when there are multiple submission types available. assignmentDetailsPage.selectSubmissionType(SubmissionType.ONLINE_URL) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt index 0d050ae7b9..40577cf944 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt @@ -29,7 +29,7 @@ import com.instructure.canvas.espresso.mockcanvas.addAssignment import com.instructure.canvas.espresso.mockcanvas.addBookmark import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager import com.instructure.canvas.espresso.mockcanvas.init -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt index 4f5545bb7f..ad74db380d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt @@ -30,7 +30,7 @@ import com.instructure.canvas.espresso.mockcanvas.addFolderToCourse import com.instructure.canvas.espresso.mockcanvas.addGroupToCourse import com.instructure.canvas.espresso.mockcanvas.addPageToCourse import com.instructure.canvas.espresso.mockcanvas.init -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.utils.refresh import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt index 0620ff5dfa..3918fb7b58 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt @@ -42,10 +42,13 @@ import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvas.espresso.mockcanvas.addAssignment import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDocumentScannerManager import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment +import com.instructure.pandautils.di.DocumentScannerModule +import com.instructure.pandautils.features.file.upload.scanner.DocumentScannerManager import com.instructure.pandautils.utils.FilePrefs import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.tokenLogin @@ -61,16 +64,23 @@ import org.junit.Test import java.io.File @HiltAndroidTest -@UninstallModules(CustomGradeStatusModule::class) +@UninstallModules(CustomGradeStatusModule::class, DocumentScannerModule::class) class PickerSubmissionUploadInteractionTest : StudentComposeTest() { @BindValue @JvmField val customGradeStatusesManager: CustomGradeStatusesManager = FakeCustomGradeStatusesManager() + private val fakeScanner = FakeDocumentScannerManager() + + @BindValue + @JvmField + val documentScannerManager: DocumentScannerManager = fakeScanner + override fun displaysPageObjects() = Unit private val mockedFileName = "sample.jpg" // A file in our assets area + private val scannerFileName = "samplepdf.pdf" // A PDF file in our assets area, because the scanner makes a PDF. private lateinit var activity : Activity private lateinit var activityResult: Instrumentation.ActivityResult @@ -82,12 +92,18 @@ class PickerSubmissionUploadInteractionTest : StudentComposeTest() { //Clear file upload cache dir. File(getInstrumentation().targetContext.cacheDir, "file_upload").deleteRecursively() - // Copy our sample file from the assets area to the external cache dir + val dir = activity.externalCacheDir + + // Copy our sample files from the assets area to the external cache dir copyAssetFileToExternalCache(activity, mockedFileName) + copyAssetFileToExternalCache(activity, scannerFileName) + + // Configure the scanner fake with the PDF file URI + fakeScanner.scannerSupported = true + fakeScanner.scanResultUri = Uri.fromFile(File(dir?.path, scannerFileName)) // Now create an ActivityResult that points to the sample file in the external cache dir val resultData = Intent() - val dir = activity.externalCacheDir val file = File(dir?.path, mockedFileName) val uri = Uri.fromFile(file) resultData.data = uri @@ -257,6 +273,33 @@ class PickerSubmissionUploadInteractionTest : StudentComposeTest() { // happy with that. } + @Test + @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) + fun testFab_scanner() { + goToSubmissionPicker() + pickerSubmissionUploadPage.chooseScanner() + pickerSubmissionUploadPage.waitForSubmitButtonToAppear() + pickerSubmissionUploadPage.assertFileDisplayed(scannerFileName) + } + + @Test + @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) + fun testFab_scannerNotAvailable() { + fakeScanner.scannerSupported = false + goToSubmissionPicker() + pickerSubmissionUploadPage.assertScannerButtonNotDisplayed() + } + + @Test + @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) + fun testDeleteFileAfterScan() { + goToSubmissionPicker() + pickerSubmissionUploadPage.chooseScanner() + pickerSubmissionUploadPage.waitForSubmitButtonToAppear() + pickerSubmissionUploadPage.clickDeleteButton() + pickerSubmissionUploadPage.assertEmptyViewDisplayed() + } + // Seed course, user, assignment and navigate to submission picker for assignment private fun goToSubmissionPicker() : MockCanvas { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt index c1aed368ad..e4442667fa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt @@ -27,6 +27,7 @@ import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions import com.instructure.canvas.espresso.mockcanvas.addRecipientsToCourse import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDashboardCoursesManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeRecentGradedSubmissionsManager @@ -42,6 +43,7 @@ import com.instructure.canvasapi2.managers.InboxSettingsManager import com.instructure.canvasapi2.managers.PostPolicyManager import com.instructure.canvasapi2.managers.SubmissionRubricManager import com.instructure.canvasapi2.managers.graphql.AssignmentDetailsManager +import com.instructure.canvasapi2.managers.graphql.DashboardCoursesManager import com.instructure.canvasapi2.managers.graphql.RecentGradedSubmissionsManager import com.instructure.canvasapi2.managers.graphql.SubmissionCommentsManager import com.instructure.canvasapi2.managers.graphql.SubmissionContentManager @@ -111,6 +113,10 @@ class StudentInboxComposeInteractionTest: InboxComposeInteractionTest() { @JvmField val recentGradedSubmissionsManager: RecentGradedSubmissionsManager = FakeRecentGradedSubmissionsManager() + @BindValue + @JvmField + val dashboardCoursesManager: DashboardCoursesManager = FakeDashboardCoursesManager() + override fun goToInboxCompose(data: MockCanvas) { val parent = data.parents.first() val token = data.tokenFor(parent)!! diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt index 554ab9a98a..156b73fc57 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt @@ -23,6 +23,7 @@ import com.instructure.canvas.espresso.common.interaction.InboxSignatureInteract import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDashboardCoursesManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager import com.instructure.canvas.espresso.mockcanvas.fakes.FakeRecentGradedSubmissionsManager @@ -38,6 +39,7 @@ import com.instructure.canvasapi2.managers.InboxSettingsManager import com.instructure.canvasapi2.managers.PostPolicyManager import com.instructure.canvasapi2.managers.SubmissionRubricManager import com.instructure.canvasapi2.managers.graphql.AssignmentDetailsManager +import com.instructure.canvasapi2.managers.graphql.DashboardCoursesManager import com.instructure.canvasapi2.managers.graphql.RecentGradedSubmissionsManager import com.instructure.canvasapi2.managers.graphql.SubmissionCommentsManager import com.instructure.canvasapi2.managers.graphql.SubmissionContentManager @@ -97,6 +99,10 @@ class StudentInboxSignatureInteractionTest : InboxSignatureInteractionTest() { @JvmField val recentGradedSubmissionsManager: RecentGradedSubmissionsManager = FakeRecentGradedSubmissionsManager() + @BindValue + @JvmField + val dashboardCoursesManager: DashboardCoursesManager = FakeDashboardCoursesManager() + private val leftSideNavigationDrawerPage = LeftSideNavigationDrawerPage() override val activityRule = StudentActivityTestRule(LoginActivity::class.java) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt index eeb180c358..4fd89c0668 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt @@ -38,9 +38,9 @@ import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Author import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.RubricCriterion -import com.instructure.canvasapi2.models.RubricCriterionRating import com.instructure.canvasapi2.models.SubmissionComment +import com.instructure.dataseeding.model.RubricCriterion +import com.instructure.dataseeding.model.RubricCriterionRating import com.instructure.espresso.triggerWorkManagerJobs import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentComposeTest @@ -155,7 +155,7 @@ class SubmissionDetailsInteractionTest : StudentComposeTest() { description = "Description of criterion", longDescription = "0, 3, 7 or 10 points", points = 10.0, - ratings = mutableListOf( + ratings = listOf( RubricCriterionRating(id="1",points=0.0,description="No Marks", longDescription = "Really?"), RubricCriterionRating(id="2",points=3.0,description="Meh", longDescription = "You're better than this!"), RubricCriterionRating(id="3",points=7.0,description="Passable", longDescription = "Getting there!"), diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnotationCommentListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnotationCommentListPage.kt index 7ea7db0296..04ec56f09f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnotationCommentListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnotationCommentListPage.kt @@ -17,7 +17,7 @@ package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.scrollRecyclerView +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/BookmarkPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/BookmarkPage.kt index 1aa45b2065..905fbc3807 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/BookmarkPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/BookmarkPage.kt @@ -27,8 +27,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import com.instructure.canvas.espresso.CanvasTest -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.scrollRecyclerView +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.espresso.assertDisplayed import com.instructure.espresso.clearText import com.instructure.espresso.click diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseBrowserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseBrowserPage.kt index a5a9fbf537..f9b1d5198b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseBrowserPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseBrowserPage.kt @@ -31,8 +31,8 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withText -import com.instructure.canvas.espresso.scrollRecyclerView -import com.instructure.canvas.espresso.withCustomConstraints +import com.instructure.canvas.espresso.utils.actionWithCustomConstraints +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Tab import com.instructure.dataseeding.model.CourseApiModel @@ -189,8 +189,8 @@ open class CourseBrowserPage : BasePage(R.id.courseBrowserPage) { // need either one or two swipe-downs to effect a refresh. We'll go with two to cover // our bases. onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayed())) - .perform(withCustomConstraints(ViewActions.swipeDown(), isDisplayingAtLeast(5))) - .perform(withCustomConstraints(ViewActions.swipeDown(), isDisplayingAtLeast(5))) + .perform(actionWithCustomConstraints(ViewActions.swipeDown(), isDisplayingAtLeast(5))) + .perform(actionWithCustomConstraints(ViewActions.swipeDown(), isDisplayingAtLeast(5))) } // When the toolbar is maximized, you can't do any operations with the recyclerView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DashboardPage.kt index de89ebf3fd..3c72d81a04 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DashboardPage.kt @@ -37,9 +37,9 @@ import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry -import com.instructure.canvas.espresso.scrollRecyclerView -import com.instructure.canvas.espresso.waitForViewToDisappear -import com.instructure.canvas.espresso.withCustomConstraints +import com.instructure.canvas.espresso.utils.actionWithCustomConstraints +import com.instructure.canvas.espresso.utils.scrollRecyclerView +import com.instructure.canvas.espresso.utils.waitForViewToDisappear import com.instructure.canvasapi2.models.AccountNotification import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group @@ -216,12 +216,12 @@ class DashboardPage : BasePage(R.id.dashboardPage) { fun selectCourse(course: Course) { assertDisplaysCourse(course) - onView(withId(R.id.titleTextView) + withText(course.originalName)).perform(withCustomConstraints(click(), isDisplayingAtLeast(10))) + onView(withId(R.id.titleTextView) + withText(course.originalName)).perform(actionWithCustomConstraints(click(), isDisplayingAtLeast(10))) } fun selectCourse(courseName: String) { assertDisplaysCourse(courseName) - onView(withId(R.id.titleTextView) + withText(courseName)).perform(withCustomConstraints(click(), isDisplayingAtLeast(10))) + onView(withId(R.id.titleTextView) + withText(courseName)).perform(actionWithCustomConstraints(click(), isDisplayingAtLeast(10))) } fun selectGroup(group: Group) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt index 159d39ea88..ff398d184e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt @@ -23,8 +23,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry -import com.instructure.canvas.espresso.scrollRecyclerView -import com.instructure.canvas.espresso.waitForMatcherWithRefreshes +import com.instructure.canvas.espresso.utils.scrollRecyclerView +import com.instructure.canvas.espresso.utils.waitForMatcherWithRefreshes import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.espresso.DoesNotExistAssertion import com.instructure.espresso.OnViewWithId diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileListPage.kt index ccd1da8500..2e125a5a9a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileListPage.kt @@ -28,9 +28,9 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withText -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.scrollRecyclerView -import com.instructure.canvas.espresso.withCustomConstraints +import com.instructure.canvas.espresso.utils.actionWithCustomConstraints +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.espresso.OnViewWithId import com.instructure.espresso.Searchable import com.instructure.espresso.assertDisplayed @@ -102,7 +102,7 @@ class FileListPage(val searchable: Searchable) : BasePage(R.id.fileListPage) { // Doesn't worry about having scrolling to the top of the page first... fun refresh() { onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayingAtLeast(50))) - .perform(withCustomConstraints(swipeDown(), isDisplayingAtLeast(10))) + .perform(actionWithCustomConstraints(swipeDown(), isDisplayingAtLeast(10))) } fun openOptionMenuFor(itemName: String) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/HelpPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/HelpPage.kt index 3913699b3f..9b58c717f0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/HelpPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/HelpPage.kt @@ -26,8 +26,8 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.instructure.canvas.espresso.StringConstants.HelpMenu -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.withCustomConstraints +import com.instructure.canvas.espresso.utils.actionWithCustomConstraints +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive import com.instructure.canvasapi2.models.Course import com.instructure.dataseeding.model.CourseApiModel import com.instructure.espresso.OnViewWithStringTextIgnoreCase @@ -49,13 +49,13 @@ class HelpPage : BasePage(R.id.helpDialog) { private val askInstructorLabel by OnViewWithText(R.string.askInstructor) private val searchGuidesLabel by OnViewWithText(R.string.searchGuides) private val reportProblemLabel by OnViewWithStringTextIgnoreCase("Report a Problem") - private val submitFeatureLabel by OnViewWithStringTextIgnoreCase("Submit a Feature Idea") + private val shareContributionLabel by OnViewWithStringTextIgnoreCase("Share a Contribution") private val shareLoveLabel by OnViewWithText(R.string.shareYourLove) fun assertAskYourInstructorDialogDetails(course: Course, question: String) { askInstructorLabel.scrollTo().click() waitForView(withText(course.name)).assertDisplayed() // Verify that our course is selected in the spinner - onView(withId(R.id.message)).scrollTo().perform(withCustomConstraints(typeText(question), isDisplayingAtLeast(1))) + onView(withId(R.id.message)).scrollTo().perform(actionWithCustomConstraints(typeText(question), isDisplayingAtLeast(1))) Espresso.closeSoftKeyboard() // Let's just make sure that the "Send" button is displayed, rather than actually pressing it onView(containsTextCaseInsensitive("Send")).assertDisplayed() @@ -64,7 +64,7 @@ class HelpPage : BasePage(R.id.helpDialog) { private fun assertAskYourInstructorDialogDetails(course: CourseApiModel, question: String) { askInstructorLabel.scrollTo().click() waitForView(withText(course.name)).assertDisplayed() // Verify that our course is selected in the spinner - onView(withId(R.id.message)).scrollTo().perform(withCustomConstraints(typeText(question), isDisplayingAtLeast(1))) + onView(withId(R.id.message)).scrollTo().perform(actionWithCustomConstraints(typeText(question), isDisplayingAtLeast(1))) Espresso.closeSoftKeyboard() // Let's just make sure that the "Send" button is displayed, rather than actually pressing it onView(containsTextCaseInsensitive("Send")).assertDisplayed() @@ -109,8 +109,8 @@ class HelpPage : BasePage(R.id.helpDialog) { shareLoveLabel.scrollTo().click() } - fun clickSubmitFeatureLabel() { - submitFeatureLabel.scrollTo().click() + fun clickShareContributionLabel() { + shareContributionLabel.scrollTo().click() } fun assertHelpMenuDisplayed() { @@ -132,8 +132,8 @@ class HelpPage : BasePage(R.id.helpDialog) { onView(withId(R.id.title) + withText(HelpMenu.REPORT_PROBLEM_TITLE)).assertDisplayed() onView(withId(R.id.subtitle) + withText(HelpMenu.REPORT_PROBLEM_SUBTITLE)).assertDisplayed() - onView(withId(R.id.title) + withText(HelpMenu.SUBMIT_FEATURE_TITLE)).assertDisplayed() - onView(withId(R.id.subtitle) + withText(HelpMenu.SUBMIT_FEATURE_SUBTITLE)).assertDisplayed() + onView(withId(R.id.title) + withText(HelpMenu.SHARE_A_CONTRIBUTION_TITLE)).assertDisplayed() + onView(withId(R.id.subtitle) + withText(HelpMenu.SHARE_A_CONTRIBUTION_SUBTITLE)).assertDisplayed() onView(withId(R.id.title) + withText(HelpMenu.SHARE_LOVE_TITLE)).assertDisplayed() onView(withId(R.id.subtitle) + withText(HelpMenu.SHARE_LOVE_SUBTITLE)).assertDisplayed() @@ -157,7 +157,7 @@ class HelpPage : BasePage(R.id.helpDialog) { when (helpMenuText) { HelpMenu.SEARCH_GUIDES_TITLE -> clickSearchGuidesLabel() - HelpMenu.SUBMIT_FEATURE_TITLE -> clickSubmitFeatureLabel() + HelpMenu.SHARE_A_CONTRIBUTION_TITLE -> clickShareContributionLabel() HelpMenu.SHARE_LOVE_TITLE -> clickShareLoveLabel() } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/LeftSideNavigationDrawerPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/LeftSideNavigationDrawerPage.kt index 70745237d8..466e4300b0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/LeftSideNavigationDrawerPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/LeftSideNavigationDrawerPage.kt @@ -9,7 +9,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withText import com.instructure.canvas.espresso.CanvasTest -import com.instructure.canvas.espresso.waitForMatcherWithSleeps +import com.instructure.canvas.espresso.utils.waitForMatcherWithSleeps import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.espresso.OnViewWithContentDescription diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt index 3a2f540655..0176440bc4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt @@ -26,9 +26,9 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withText -import com.instructure.canvas.espresso.ImageViewDrawableMatcher -import com.instructure.canvas.espresso.scrollRecyclerView -import com.instructure.canvas.espresso.withCustomConstraints +import com.instructure.canvas.espresso.utils.ImageViewDrawableMatcher +import com.instructure.canvas.espresso.utils.actionWithCustomConstraints +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.ModuleObject @@ -183,7 +183,7 @@ class ModulesPage : BasePage(R.id.modulesPage) { } fun refresh() { - onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayed())).perform(withCustomConstraints(swipeDown(), isDisplayingAtLeast(5))) + onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayed())).perform(actionWithCustomConstraints(swipeDown(), isDisplayingAtLeast(5))) } fun clickOnModuleExpandCollapseIcon(moduleName: String) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/NotificationPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/NotificationPage.kt index bdc40097a5..c240353169 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/NotificationPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/NotificationPage.kt @@ -20,9 +20,9 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers.hasSibling -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.refresh -import com.instructure.canvas.espresso.scrollRecyclerView +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.refresh +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.espresso.RecyclerViewItemCountGreaterThanAssertion import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click @@ -39,8 +39,12 @@ import org.hamcrest.Matchers class NotificationPage : BasePage() { - fun assertNotificationDisplayed(title: String) { - val matcher = allOf(containsTextCaseInsensitive(title), withId(R.id.title)) + fun assertNotificationDisplayed(title: String, contains: Boolean = false) { + val matcher = if (contains) { + allOf(containsTextCaseInsensitive(title), withId(R.id.title)) + } else { + allOf(withText(title), withId(R.id.title)) + } scrollRecyclerView(R.id.listView, matcher) onView(matcher).assertDisplayed() } @@ -70,8 +74,12 @@ class NotificationPage : BasePage() { onView(matcher).scrollTo().assertDisplayed() } - fun clickNotification(title: String) { - val matcher = allOf(containsTextCaseInsensitive(title), withId(R.id.title)) + fun clickNotification(title: String, contains: Boolean = false) { + val matcher = if (contains) { + allOf(containsTextCaseInsensitive(title), withId(R.id.title)) + } else { + allOf(withText(title), withId(R.id.title)) + } scrollRecyclerView(R.id.listView, matcher) onView(matcher).click() } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageListPage.kt index 0424bc4619..d33a9e5ab3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageListPage.kt @@ -20,7 +20,7 @@ import android.view.View import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.withText -import com.instructure.canvas.espresso.scrollRecyclerView +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.canvasapi2.models.Page import com.instructure.dataseeding.model.PageApiModel import com.instructure.espresso.DoesNotExistAssertion diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PairObserverPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PairObserverPage.kt index d692cd4016..1a6313f582 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PairObserverPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PairObserverPage.kt @@ -18,8 +18,8 @@ package com.instructure.student.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.getText -import com.instructure.canvas.espresso.matchToolbarText +import com.instructure.canvas.espresso.utils.getText +import com.instructure.canvas.espresso.utils.matchToolbarText import com.instructure.espresso.OnViewWithId import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.assertDisplayed diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PandaAvatarPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PandaAvatarPage.kt index 235df4a65d..d777dc428e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PandaAvatarPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PandaAvatarPage.kt @@ -2,7 +2,7 @@ package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withText -import com.instructure.canvas.espresso.stringContainsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.stringContainsTextCaseInsensitive import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PeopleListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PeopleListPage.kt index f8814d9c37..0648a5d49c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PeopleListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PeopleListPage.kt @@ -23,7 +23,7 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import com.instructure.canvas.espresso.getViewChildCountWithoutId +import com.instructure.canvas.espresso.utils.getViewChildCountWithoutId import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.espresso.OnViewWithId diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt index 3c96301b25..0eaf1ff798 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt @@ -16,12 +16,13 @@ */ package com.instructure.student.ui.pages.classic +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertNotDisplayed import com.instructure.espresso.click import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onView import com.instructure.espresso.page.waitForViewWithText import com.instructure.espresso.page.withId import com.instructure.espresso.page.withText @@ -33,6 +34,7 @@ class PickerSubmissionUploadPage : BasePage(R.id.pickerSubmissionUploadPage) { private val deviceIcon by OnViewWithId(R.id.sourceDeviceIcon) private val cameraIcon by OnViewWithId(R.id.sourceCameraIcon) private val galleryIcon by OnViewWithId(R.id.sourceGalleryIcon) + private val scannerIcon by OnViewWithId(R.id.sourceScannerIcon) private val deleteButton by OnViewWithId(R.id.deleteButton) fun chooseDevice() { @@ -47,6 +49,18 @@ class PickerSubmissionUploadPage : BasePage(R.id.pickerSubmissionUploadPage) { galleryIcon.click() } + fun chooseScanner() { + scannerIcon.click() + } + + fun assertScannerButtonDisplayed() { + onView(withId(R.id.sourceScanner)).assertDisplayed() + } + + fun assertScannerButtonNotDisplayed() { + onView(withId(R.id.sourceScanner)).assertNotDisplayed() + } + fun waitForSubmitButtonToAppear() { waitForViewWithText(R.string.submit) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ProfileSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ProfileSettingsPage.kt index 750c32bd45..a4e9a7499d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ProfileSettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ProfileSettingsPage.kt @@ -8,7 +8,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.withId import com.instructure.canvas.espresso.CanvasTest -import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive import com.instructure.espresso.OnViewWithId import com.instructure.espresso.click import com.instructure.espresso.page.BasePage diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizListPage.kt index bbe2201c8b..55f7ad2a18 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizListPage.kt @@ -23,8 +23,8 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withText -import com.instructure.canvas.espresso.scrollRecyclerView -import com.instructure.canvas.espresso.withCustomConstraints +import com.instructure.canvas.espresso.utils.actionWithCustomConstraints +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.canvasapi2.models.Quiz import com.instructure.dataseeding.model.QuizApiModel import com.instructure.espresso.RecyclerViewItemCountAssertion @@ -93,7 +93,7 @@ class QuizListPage(val searchable: Searchable) : BasePage(R.id.quizListPage) { fun refresh() { onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayed())) - .perform(withCustomConstraints(swipeDown(), isDisplayingAtLeast(10))) + .perform(actionWithCustomConstraints(swipeDown(), isDisplayingAtLeast(10))) } fun assertPointsDisplayed(points: String?) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/RemoteConfigSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/RemoteConfigSettingsPage.kt index 73ea2f4b18..3430b842ef 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/RemoteConfigSettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/RemoteConfigSettingsPage.kt @@ -21,9 +21,9 @@ import android.widget.EditText import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.clearFocus -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.scrollRecyclerView +import com.instructure.canvas.espresso.utils.clearFocus +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.canvasapi2.utils.RemoteConfigParam import com.instructure.espresso.click import com.instructure.espresso.page.BasePage diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt index a84e6f8708..e5e5816906 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt @@ -27,7 +27,7 @@ import androidx.test.espresso.action.ViewActions import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage -import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive import com.instructure.dataseeding.model.SubmissionType import com.instructure.espresso.ModuleItemInteractions import com.instructure.espresso.assertDisplayed diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsPage.kt index ca13645290..4921f25378 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsPage.kt @@ -28,21 +28,24 @@ import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.isActivated import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast +import androidx.test.espresso.matcher.ViewMatchers.isSelected import androidx.test.espresso.matcher.ViewMatchers.withText 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.canvas.espresso.clickCoordinates -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.scrollRecyclerView -import com.instructure.canvas.espresso.withCustomConstraints -import com.instructure.canvasapi2.models.RubricCriterion +import com.instructure.canvas.espresso.utils.actionWithCustomConstraints +import com.instructure.canvas.espresso.utils.clickCoordinates +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.dataseeding.model.RubricCriterion +import com.instructure.dataseeding.model.RubricCriterionRating import com.instructure.espresso.OnViewWithStringTextIgnoreCase import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click @@ -62,6 +65,7 @@ import com.instructure.student.ui.rendertests.renderpages.SubmissionCommentsRend import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.anyOf import org.hamcrest.Matchers.containsString +import org.hamcrest.Matchers.not import java.lang.Thread.sleep open class SubmissionDetailsPage : BasePage(R.id.submissionDetails) { @@ -228,7 +232,7 @@ open class SubmissionDetailsPage : BasePage(R.id.submissionDetails) { if(click) { //onView(commentMatcher).click() onView(allOf(withId(R.id.attachmentNameTextView), withText(fileName))) - .perform(withCustomConstraints(click(), isDisplayingAtLeast(5))) + .perform(actionWithCustomConstraints(click(), isDisplayingAtLeast(5))) } } @@ -259,6 +263,10 @@ open class SubmissionDetailsPage : BasePage(R.id.submissionDetails) { swipeDrawerTo(GeneralLocation.BOTTOM_CENTER) } + fun expandSlidingPanel() { + swipeDrawerTo(GeneralLocation.TOP_CENTER) + } + fun addAndSendComment(comment: String) { submissionCommentsRenderPage.addAndSendComment(comment) } @@ -282,17 +290,14 @@ open class SubmissionDetailsPage : BasePage(R.id.submissionDetails) { fun assertRubricCriterionDisplayed(rc: RubricCriterion) { rc.ratings.forEach { rating -> val matcher = allOf(withParent(withId(R.id.ratingLayout)), withText(rating.points.toInt().toString())) - scrollRecyclerView(R.id.recyclerView, matcher) onView(matcher).assertDisplayed() onView(matcher).click() val descriptionMatcher = allOf(withId(R.id.ratingTitle), withText(rating.description)) - scrollRecyclerView(R.id.recyclerView, descriptionMatcher) onView(descriptionMatcher).check(matches(isDisplayingAtLeast(10))) if(rating.longDescription != null) { val longDescriptionMatcher = allOf(withId(R.id.ratingDescription), withText(rating.longDescription)) - scrollRecyclerView(R.id.recyclerView, longDescriptionMatcher) onView(longDescriptionMatcher).check(matches(isDisplayingAtLeast(10))) } } @@ -302,9 +307,10 @@ open class SubmissionDetailsPage : BasePage(R.id.submissionDetails) { * Checks that pressing the "Description" button pops up a webview with the longDescription text */ fun assertRubricDescriptionDisplays(rc: RubricCriterion) { - val matcher = allOf(withId(R.id.descriptionButton), containsTextCaseInsensitive("description")) - scrollRecyclerView(R.id.recyclerView, matcher) - onView(matcher).assertDisplayed() // probably unnecessary + val matcher = allOf( + withId(R.id.descriptionButton), + withAncestor(allOf(withId(R.id.rubricCriterion), hasDescendant(allOf(withId(R.id.criterionTitle), withText(rc.description))))) + ) onView(matcher).click() onWebView(withId(R.id.webView)) @@ -315,6 +321,40 @@ open class SubmissionDetailsPage : BasePage(R.id.submissionDetails) { } + fun assertRubricRatingSelected(rc: RubricCriterion, rating: RubricCriterionRating) { + val criterionAncestor = allOf(withId(R.id.rubricCriterion), hasDescendant(allOf(withId(R.id.criterionTitle), withText(rc.description)))) + onView(allOf(withId(R.id.ratingTitle), withText(rating.description), withAncestor(criterionAncestor))) + .check(matches(isDisplayingAtLeast(10))) + if (rating.longDescription != null) { + onView(allOf(withId(R.id.ratingDescription), withText(rating.longDescription), withAncestor(criterionAncestor))) + .check(matches(isDisplayingAtLeast(10))) + } + } + + fun assertRubricCustomScoreSelected(rc: RubricCriterion) { + val criterionAncestor = allOf(withId(R.id.rubricCriterion), hasDescendant(allOf(withId(R.id.criterionTitle), withText(rc.description)))) + onView(allOf(withId(R.id.ratingTitle), withText(R.string.rubricCustomScore), withAncestor(criterionAncestor))) + .check(matches(isDisplayingAtLeast(10))) + } + + fun clickRubricRating(rc: RubricCriterion, rating: RubricCriterionRating) { + val criterionAncestor = allOf(withId(R.id.rubricCriterion), hasDescendant(allOf(withId(R.id.criterionTitle), withText(rc.description)))) + onView(allOf(withParent(withId(R.id.ratingLayout)), withText(rating.points.toInt().toString()), withAncestor(criterionAncestor))).click() + } + + fun assertRubricRatingIsAssessed(rc: RubricCriterion, rating: RubricCriterionRating) { + val criterionAncestor = allOf(withId(R.id.rubricCriterion), hasDescendant(allOf(withId(R.id.criterionTitle), withText(rc.description)))) + onView(allOf(withParent(withId(R.id.ratingLayout)), withText(rating.points.toInt().toString()), withAncestor(criterionAncestor))) + .check(matches(isActivated())) + } + + fun assertRubricRatingIsPreviewSelected(rc: RubricCriterion, rating: RubricCriterionRating) { + val criterionAncestor = allOf(withId(R.id.rubricCriterion), hasDescendant(allOf(withId(R.id.criterionTitle), withText(rc.description)))) + val ratingMatcher = allOf(withParent(withId(R.id.ratingLayout)), withText(rating.points.toInt().toString()), withAncestor(criterionAncestor)) + onView(ratingMatcher).check(matches(isSelected())) + onView(ratingMatcher).check(matches(not(isActivated()))) + } + fun assertNoSubmissionEmptyView() { onView(allOf(withId(R.id.title), withText(R.string.submissionDetailsNoSubmissionYet), withAncestor(withId(R.id.submissionDetailsEmptyContent)))).assertDisplayed() } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SyllabusPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SyllabusPage.kt index 7fc11742d9..7500c55fc0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SyllabusPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SyllabusPage.kt @@ -24,9 +24,9 @@ import androidx.test.espresso.web.assertion.WebViewAssertions import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.webdriver.DriverAtoms import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.matchToolbarText -import com.instructure.canvas.espresso.scrollRecyclerView +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.matchToolbarText +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click import com.instructure.espresso.page.BasePage diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ImportantDatesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ImportantDatesPage.kt index 6a7e466a4c..444065deaa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ImportantDatesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ImportantDatesPage.kt @@ -21,7 +21,7 @@ import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.countConstraintLayoutsInRecyclerView +import com.instructure.canvas.espresso.utils.countConstraintLayoutsInRecyclerView import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.assertHasChild diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/ManageOfflineContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/ManageOfflineContentPage.kt index e54b83838d..a0f162c6e5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/ManageOfflineContentPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/ManageOfflineContentPage.kt @@ -24,9 +24,9 @@ import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.hasCheckedState -import com.instructure.canvas.espresso.withRotation +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.hasCheckedState +import com.instructure.canvas.espresso.utils.withRotation import com.instructure.espresso.ConstraintLayoutItemCountAssertion import com.instructure.espresso.ConstraintLayoutItemCountAssertionWithMatcher import com.instructure.espresso.DoesNotExistAssertion diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/NativeDiscussionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/NativeDiscussionDetailsPage.kt index ee17271db8..f5781ea21f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/NativeDiscussionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/NativeDiscussionDetailsPage.kt @@ -29,10 +29,10 @@ import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.DriverAtoms.getText import androidx.test.espresso.web.webdriver.DriverAtoms.webClick import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.isElementDisplayed -import com.instructure.canvas.espresso.waitForMatcherWithSleeps -import com.instructure.canvas.espresso.withCustomConstraints +import com.instructure.canvas.espresso.utils.actionWithCustomConstraints +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.isElementDisplayed +import com.instructure.canvas.espresso.utils.waitForMatcherWithSleeps import com.instructure.canvas.espresso.withElementRepeat import com.instructure.canvasapi2.models.DiscussionEntry import com.instructure.canvasapi2.models.DiscussionTopicHeader @@ -86,7 +86,7 @@ class NativeDiscussionDetailsPage(val moduleItemInteractions: ModuleItemInteract fun refresh() { scrollToTop() onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayingAtLeast(10))) - .perform(withCustomConstraints(swipeDown(), isDisplayingAtLeast(10))) + .perform(actionWithCustomConstraints(swipeDown(), isDisplayingAtLeast(10))) } fun scrollToRepliesWebview() { @@ -348,6 +348,6 @@ class NativeDiscussionDetailsPage(val moduleItemInteractions: ModuleItemInteract private fun scrollToTop() { onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayingAtLeast(10))) - .perform(withCustomConstraints(swipeDown(), isDisplayingAtLeast(10))) + .perform(actionWithCustomConstraints(swipeDown(), isDisplayingAtLeast(10))) } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/SyncProgressPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/SyncProgressPage.kt index 35e251c694..5b34077de8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/SyncProgressPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/SyncProgressPage.kt @@ -20,8 +20,8 @@ package com.instructure.student.ui.pages.classic.offline import android.widget.TextView import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.hasSibling -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.getView +import com.instructure.canvas.espresso.utils.containsTextCaseInsensitive +import com.instructure.canvas.espresso.utils.getView import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertContainsText import com.instructure.espresso.assertDisplayed diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/compose/TextSubmissionUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/compose/TextSubmissionUploadPage.kt index a7f389471c..5663e3288b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/compose/TextSubmissionUploadPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/compose/TextSubmissionUploadPage.kt @@ -17,8 +17,8 @@ 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.canvas.espresso.utils.TypeInRCETextEditor +import com.instructure.canvas.espresso.utils.explicitClick import com.instructure.composetest.clickToolbarIconButton import com.instructure.espresso.OnViewWithId import com.instructure.espresso.OnViewWithText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/DashboardScreenTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/DashboardScreenTest.kt index 10db653702..78aa56c021 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/DashboardScreenTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/DashboardScreenTest.kt @@ -23,11 +23,11 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter +import com.instructure.pandautils.features.dashboard.DashboardNavigationEvent +import com.instructure.pandautils.features.dashboard.DashboardNavigationHandler +import com.instructure.pandautils.features.dashboard.compose.DashboardUiState import com.instructure.pandautils.features.dashboard.widget.WidgetMetadata import com.instructure.student.features.dashboard.compose.DashboardScreenContent -import com.instructure.student.features.dashboard.compose.DashboardUiState import kotlinx.coroutines.flow.MutableSharedFlow import org.junit.Assert.assertTrue import org.junit.Rule @@ -40,14 +40,13 @@ class DashboardScreenTest { @get:Rule val composeTestRule = createComposeRule() - private val mockRouter = object : DashboardRouter { - override fun routeToGlobalAnnouncement(subject: String, message: String) {} - override fun routeToSubmissionDetails(canvasContext: CanvasContext, assignmentId: Long, attemptId: Long) {} - override fun routeToMyFiles(canvasContext: CanvasContext, folderId: Long) {} - override fun routeToSyncProgress() {} - override fun routeToManageOfflineContent() {} - override fun routeToCustomizeDashboard() {} - override fun restartApp() {} + private val mockNavigationHandler = object : DashboardNavigationHandler { + override fun handleCoursesNavigation(event: DashboardNavigationEvent.Courses) {} + override fun handleTodoNavigation(event: DashboardNavigationEvent.Todo) {} + override fun handleForecastNavigation(event: DashboardNavigationEvent.Forecast) {} + override fun handleProgressNavigation(event: DashboardNavigationEvent.Progress) {} + override fun handleConferencesNavigation(event: DashboardNavigationEvent.Conferences) {} + override fun handleDashboardNavigation(event: DashboardNavigationEvent.Dashboard) {} } @Test @@ -66,7 +65,7 @@ class DashboardScreenTest { refreshSignal = MutableSharedFlow(), snackbarMessageFlow = MutableSharedFlow(), onShowSnackbar = { _, _, _ -> }, - router = mockRouter + navigationHandler = mockNavigationHandler ) } @@ -90,7 +89,7 @@ class DashboardScreenTest { refreshSignal = MutableSharedFlow(), snackbarMessageFlow = MutableSharedFlow(), onShowSnackbar = { _, _, _ -> }, - router = mockRouter + navigationHandler = mockNavigationHandler ) } @@ -114,7 +113,7 @@ class DashboardScreenTest { refreshSignal = MutableSharedFlow(), snackbarMessageFlow = MutableSharedFlow(), onShowSnackbar = { _, _, _ -> }, - router = mockRouter + navigationHandler = mockNavigationHandler ) } @@ -138,7 +137,7 @@ class DashboardScreenTest { refreshSignal = MutableSharedFlow(), snackbarMessageFlow = MutableSharedFlow(), onShowSnackbar = { _, _, _ -> }, - router = mockRouter + navigationHandler = mockNavigationHandler ) } @@ -167,7 +166,7 @@ class DashboardScreenTest { refreshSignal = MutableSharedFlow(), snackbarMessageFlow = MutableSharedFlow(), onShowSnackbar = { _, _, _ -> }, - router = mockRouter + navigationHandler = mockNavigationHandler ) } @@ -176,18 +175,19 @@ class DashboardScreenTest { } @Test - fun testCustomizeDashboardButtonCallsRouter() { - var routerCalled = false - val testRouter = object : DashboardRouter { - override fun routeToGlobalAnnouncement(subject: String, message: String) {} - override fun routeToSubmissionDetails(canvasContext: CanvasContext, assignmentId: Long, attemptId: Long) {} - override fun routeToMyFiles(canvasContext: CanvasContext, folderId: Long) {} - override fun routeToSyncProgress() {} - override fun routeToManageOfflineContent() {} - override fun routeToCustomizeDashboard() { - routerCalled = true + fun testCustomizeDashboardButtonCallsNavigationHandler() { + var navigationCalled = false + val testNavigationHandler = object : DashboardNavigationHandler { + override fun handleCoursesNavigation(event: DashboardNavigationEvent.Courses) {} + override fun handleTodoNavigation(event: DashboardNavigationEvent.Todo) {} + override fun handleForecastNavigation(event: DashboardNavigationEvent.Forecast) {} + override fun handleProgressNavigation(event: DashboardNavigationEvent.Progress) {} + override fun handleConferencesNavigation(event: DashboardNavigationEvent.Conferences) {} + override fun handleDashboardNavigation(event: DashboardNavigationEvent.Dashboard) { + if (event is DashboardNavigationEvent.Dashboard.NavigateToCustomizeDashboard) { + navigationCalled = true + } } - override fun restartApp() {} } val mockWidgets = listOf( @@ -208,13 +208,13 @@ class DashboardScreenTest { refreshSignal = MutableSharedFlow(), snackbarMessageFlow = MutableSharedFlow(), onShowSnackbar = { _, _, _ -> }, - router = testRouter + navigationHandler = testNavigationHandler ) } composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Customize Dashboard").performClick() - assertTrue("Router's routeToCustomizeDashboard should be called", routerCalled) + assertTrue("NavigationHandler's handleDashboardNavigation should be called", navigationCalled) } -} \ No newline at end of file +} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionRubricRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionRubricRenderTest.kt index e9c3e63113..96887e33d3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionRubricRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionRubricRenderTest.kt @@ -22,7 +22,7 @@ import androidx.test.espresso.matcher.RootMatchers import androidx.test.espresso.matcher.ViewMatchers.hasChildCount import androidx.test.espresso.matcher.ViewMatchers.isSelected import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.canvas.espresso.assertFontSizeSP +import com.instructure.canvas.espresso.utils.assertFontSizeSP import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.RubricCriterion import com.instructure.canvasapi2.models.RubricCriterionRating diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceDetailsRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceDetailsRenderPage.kt index 4a58351281..6ad7cb93ae 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceDetailsRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceDetailsRenderPage.kt @@ -18,7 +18,7 @@ package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withAlpha -import com.instructure.canvas.espresso.assertIsRefreshing +import com.instructure.canvas.espresso.utils.assertIsRefreshing import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.assertGone diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceListRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceListRenderPage.kt index afe553df23..403a323975 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceListRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceListRenderPage.kt @@ -17,8 +17,8 @@ package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions.doesNotExist -import com.instructure.canvas.espresso.assertIsRefreshing -import com.instructure.canvas.espresso.scrollRecyclerView +import com.instructure.canvas.espresso.utils.assertIsRefreshing +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.assertGone diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PairObserverRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PairObserverRenderPage.kt index c08fb787ce..64a52656dd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PairObserverRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PairObserverRenderPage.kt @@ -16,7 +16,7 @@ */ package com.instructure.student.ui.rendertests.renderpages -import com.instructure.canvas.espresso.waitForMatcherWithSleeps +import com.instructure.canvas.espresso.utils.waitForMatcherWithSleeps import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.assertGone diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionCommentsRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionCommentsRenderPage.kt index bca33cea82..0e2f6f62be 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionCommentsRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionCommentsRenderPage.kt @@ -23,8 +23,8 @@ import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText -import com.instructure.canvas.espresso.DirectlyPopulateEditText -import com.instructure.canvas.espresso.scrollRecyclerView +import com.instructure.canvas.espresso.utils.DirectlyPopulateEditText +import com.instructure.canvas.espresso.utils.scrollRecyclerView import com.instructure.canvasapi2.utils.Pronouns import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/extensions/StudentTestExtensions.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/extensions/StudentTestExtensions.kt index 1f74691925..7aa6f0d5dd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/extensions/StudentTestExtensions.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/extensions/StudentTestExtensions.kt @@ -24,19 +24,18 @@ import android.net.Uri import android.os.Environment import androidx.fragment.app.FragmentActivity import androidx.test.espresso.Espresso -import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.platform.app.InstrumentationRegistry import com.instructure.canvas.espresso.CanvasTest -import com.instructure.canvas.espresso.waitForMatcherWithSleeps +import com.instructure.canvas.espresso.utils.waitForMatcherWithSleeps import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.dataseeding.api.FileUploadsApi +import com.instructure.dataseeding.api.RubricsApi import com.instructure.dataseeding.api.SeedApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.api.UserApi @@ -46,6 +45,8 @@ import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.EnrollmentTypes import com.instructure.dataseeding.model.FileType import com.instructure.dataseeding.model.FileUploadType +import com.instructure.dataseeding.model.RubricApiModel +import com.instructure.dataseeding.model.RubricCriterion import com.instructure.dataseeding.model.SubmissionApiModel import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.CanvasNetworkAdapter @@ -207,9 +208,9 @@ fun CanvasTest.tokenLogin(domain: String, token: String, user: User) { } // Sometimes, especially on slow FTL emulators, it can take a bit for the dashboard to show // up after a token login. Add some tolerance for that. - waitForMatcherWithSleeps(ViewMatchers.withId(R.id.dashboardPage), 20000).check( - ViewAssertions.matches( - ViewMatchers.isDisplayed() + waitForMatcherWithSleeps(withId(R.id.dashboardPage), 20000).check( + matches( + isDisplayed() ) ) } @@ -343,4 +344,22 @@ fun uploadTextFile( token, fileUploadType ) +} + +fun seedAssignmentWithRubric( + courseId: Long, + assignmentId: Long, + teacherToken: String, + title: String = "Test Rubric", + criteria: List +): RubricApiModel { + val created = RubricsApi.createAssignmentWithRubric( + courseId = courseId, + assignmentId = assignmentId, + teacherToken = teacherToken, + title = title, + criteria = criteria + ) + val assignment = AssignmentsApi.getAssignment(courseId, assignmentId, teacherToken) + return created.copy(criteria = assignment.rubric ?: emptyList()) } \ No newline at end of file diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml index eb4b93f739..33cb243a4e 100644 --- a/apps/student/src/main/AndroidManifest.xml +++ b/apps/student/src/main/AndroidManifest.xml @@ -280,6 +280,14 @@ + +