diff --git a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml
index 7968bc38fe..92cc44acbe 100644
--- a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml
+++ b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml
@@ -37,7 +37,7 @@
-
+
diff --git a/apps/flutter_parent/lib/screens/courses/courses_screen.dart b/apps/flutter_parent/lib/screens/courses/courses_screen.dart
index c3484287c7..36ab053186 100644
--- a/apps/flutter_parent/lib/screens/courses/courses_screen.dart
+++ b/apps/flutter_parent/lib/screens/courses/courses_screen.dart
@@ -130,11 +130,14 @@ class _CoursesScreenState extends State {
// If there is no current grade, return 'No grade'
// Otherwise, we have a grade, so check if we have the actual grade string
// or a score
+ var formattedScore = (grade.currentScore() != null && !(course.settings?.restrictQuantitativeData ?? false))
+ ? format.format(grade.currentScore()! / 100)
+ : '';
var text = grade.noCurrentGrade()
? L10n(context).noGrade
: grade.currentGrade()?.isNotEmpty == true
- ? grade.currentGrade()!
- : format.format(grade.currentScore()! / 100);
+ ? "${grade.currentGrade()}${formattedScore.isNotEmpty ? ' $formattedScore' : ''}"
+ : formattedScore;
return Text(
text,
diff --git a/apps/flutter_parent/lib/screens/courses/details/course_grades_screen.dart b/apps/flutter_parent/lib/screens/courses/details/course_grades_screen.dart
index 97e7c9222e..4163980c2a 100644
--- a/apps/flutter_parent/lib/screens/courses/details/course_grades_screen.dart
+++ b/apps/flutter_parent/lib/screens/courses/details/course_grades_screen.dart
@@ -255,22 +255,25 @@ class _CourseGradeHeader extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(L10n(context).courseTotalGradeLabel, style: textTheme.bodyMedium),
- Text(_courseGrade(context, grade), style: textTheme.bodyMedium, key: Key("total_grade")),
+ Text(_courseGrade(context, grade, model.courseSettings?.restrictQuantitativeData ?? false), style: textTheme.bodyMedium, key: Key("total_grade")),
],
),
);
}
- String _courseGrade(BuildContext context, CourseGrade grade) {
+ String _courseGrade(BuildContext context, CourseGrade grade, bool restrictQuantitativeData) {
final format = NumberFormat.percentPattern();
format.maximumFractionDigits = 2;
if (grade.noCurrentGrade()) {
return L10n(context).noGrade;
} else {
+ var formattedScore = (grade.currentScore() != null && restrictQuantitativeData == false)
+ ? format.format(grade.currentScore()! / 100)
+ : '';
return grade.currentGrade()?.isNotEmpty == true
- ? grade.currentGrade()!
- : format.format(grade.currentScore()! / 100); // format multiplies by 100 for percentages
+ ? "${grade.currentGrade()}${formattedScore.isNotEmpty ? ' $formattedScore' : ''}"
+ : formattedScore;
}
}
}
@@ -375,7 +378,7 @@ class _AssignmentRow extends StatelessWidget {
final submission = assignment.submission(studentId);
- final restrictQuantitativeData = course?.settings?.restrictQuantitativeData ?? false;
+ final restrictQuantitativeData = course.settings?.restrictQuantitativeData ?? false;
if (submission?.excused ?? false) {
text = restrictQuantitativeData
diff --git a/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart b/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart
index 7ef1f02a8a..9ece106217 100644
--- a/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart
+++ b/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart
@@ -427,6 +427,30 @@ void main() {
expect(find.text(grade), findsOneWidget);
});
+ testWidgetsWithAccessibilityChecks('from current grade and score', (tester) async {
+ final grade = 'Big fat F';
+ final score = 15.15;
+ final groups = [
+ _mockAssignmentGroup(assignments: [_mockAssignment()])
+ ];
+ final enrollment = Enrollment((b) => b
+ ..enrollmentState = 'active'
+ ..grades = _mockGrade(currentScore: score, currentGrade: grade));
+ final model = CourseDetailsModel(_student, _courseId);
+ model.course = _mockCourse();
+ when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups);
+ when(interactor.loadGradingPeriods(_courseId)).thenAnswer((_) async =>
+ GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder()));
+ when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null))
+ .thenAnswer((_) async => [enrollment]);
+
+ await tester.pumpWidget(_testableWidget(model));
+ await tester.pump(); // Build the widget
+ await tester.pump(); // Let the future finish
+
+ expect(find.text("$grade $score%"), findsOneWidget);
+ });
+
testWidgetsWithAccessibilityChecks('is not shown when locked', (tester) async {
final groups = [
_mockAssignmentGroup(assignments: [_mockAssignment()])
@@ -497,6 +521,31 @@ void main() {
expect(find.text(grade), findsOneWidget);
});
+ testWidgetsWithAccessibilityChecks('only grade is shown when restricted', (tester) async {
+ final grade = 'Big fat F';
+ final score = 15.15;
+ final groups = [
+ _mockAssignmentGroup(assignments: [_mockAssignment()])
+ ];
+ final enrollment = Enrollment((b) => b
+ ..enrollmentState = 'active'
+ ..grades = _mockGrade(currentScore: score, currentGrade: grade));
+ final model = CourseDetailsModel(_student, _courseId);
+ model.course = _mockCourse();
+ model.courseSettings = CourseSettings((b) => b..restrictQuantitativeData = true);
+ when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups);
+ when(interactor.loadGradingPeriods(_courseId))
+ .thenAnswer((_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder()));
+ when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]);
+
+ await tester.pumpWidget(_testableWidget(model));
+ await tester.pump(); // Build the widget
+ await tester.pump(); // Let the future finish
+
+ // Verify that we are showing the course grade when restricted
+ expect(find.text(grade), findsOneWidget);
+ });
+
testWidgetsWithAccessibilityChecks('is shown when looking at a grading period', (tester) async {
final groups = [
_mockAssignmentGroup(assignments: [_mockAssignment()])
@@ -780,7 +829,7 @@ Course _mockCourse() {
GradeBuilder _mockGrade({double? currentScore, double? finalScore, String? currentGrade, String? finalGrade}) {
return GradeBuilder()
..htmlUrl = ''
- ..currentScore = currentScore ?? 0
+ ..currentScore = currentScore
..finalScore = finalScore ?? 0
..currentGrade = currentGrade ?? ''
..finalGrade = finalGrade ?? '';
diff --git a/apps/flutter_parent/test/screens/courses/courses_screen_test.dart b/apps/flutter_parent/test/screens/courses/courses_screen_test.dart
index 864d68f7de..6dde78c2a0 100644
--- a/apps/flutter_parent/test/screens/courses/courses_screen_test.dart
+++ b/apps/flutter_parent/test/screens/courses/courses_screen_test.dart
@@ -41,7 +41,6 @@ import 'package:provider/provider.dart';
import '../../utils/accessibility_utils.dart';
import '../../utils/platform_config.dart';
import '../../utils/test_app.dart';
-import '../../utils/test_helpers/mock_helpers.dart';
import '../../utils/test_helpers/mock_helpers.mocks.dart';
void main() {
@@ -174,6 +173,48 @@ void main() {
expect(gradeWidget, findsNWidgets(courses.length));
});
+ testWidgetsWithAccessibilityChecks('shows grade and score if there is a current grade and score', (tester) async {
+ var student = _mockStudent('1');
+ var courses = List.generate(
+ 1,
+ (idx) => _mockCourse(
+ idx.toString(),
+ enrollments: ListBuilder(
+ [_mockEnrollment(idx.toString(), userId: student.id, computedCurrentGrade: 'A', computedCurrentScore: 75)],
+ ),
+ ),
+ );
+
+ _setupLocator(_MockedCoursesInteractor(courses: courses));
+
+ await tester.pumpWidget(_testableMaterialWidget());
+ await tester.pumpAndSettle();
+
+ final gradeWidget = find.text('A 75%');
+ expect(gradeWidget, findsNWidgets(courses.length));
+ });
+
+ testWidgetsWithAccessibilityChecks('shows grade only if there is a current grade and score and restricted', (tester) async {
+ var student = _mockStudent('1');
+ var courses = List.generate(
+ 1,
+ (idx) => _mockCourse(
+ idx.toString(),
+ enrollments: ListBuilder(
+ [_mockEnrollment(idx.toString(), userId: student.id, computedCurrentGrade: 'A', computedCurrentScore: 75)],
+ ),
+ ).rebuild((b) => b..settings = (b.settings..restrictQuantitativeData = true)),
+ );
+
+ _setupLocator(_MockedCoursesInteractor(courses: courses));
+
+ await tester.pumpWidget(_testableMaterialWidget());
+ await tester.pumpAndSettle();
+
+ final gradeWidget = find.text('A');
+ expect(gradeWidget, findsNWidgets(courses.length));
+ });
+
testWidgetsWithAccessibilityChecks('shows score if there is a grade but no grade string', (tester) async {
var student = _mockStudent('1');
var courses = List.generate(
diff --git a/apps/settings.gradle b/apps/settings.gradle
index 9fff40bf18..8486e179b4 100644
--- a/apps/settings.gradle
+++ b/apps/settings.gradle
@@ -1,7 +1,20 @@
+pluginManagement {
+ buildscript {
+ repositories {
+ mavenCentral()
+ maven {
+ url = uri("https://storage.googleapis.com/r8-releases/raw")
+ }
+ }
+ dependencies {
+ classpath("com.android.tools:r8:8.2.47")
+ }
+ }
+}
+
/* Top-level project modules */
include ':student'
include ':teacher'
-
/* Flutter embed modules */
setBinding(new Binding([gradle: this]))
@@ -36,4 +49,4 @@ project(':pandautils').projectDir = new File(rootProject.projectDir, '/../libs/p
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(':DocumentScanner').projectDir = new File(rootProject.projectDir, '/../libs/DocumentScanner')
+project(':DocumentScanner').projectDir = new File(rootProject.projectDir, '/../libs/DocumentScanner')
\ No newline at end of file
diff --git a/apps/student/build.gradle b/apps/student/build.gradle
index f27f2dd8e4..c487133e6d 100644
--- a/apps/student/build.gradle
+++ b/apps/student/build.gradle
@@ -50,8 +50,8 @@ android {
applicationId "com.instructure.candroid"
minSdkVersion Versions.MIN_SDK
targetSdkVersion Versions.TARGET_SDK
- versionCode = 258
- versionName = '7.0.2'
+ versionCode = 259
+ versionName = '7.1.0'
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
@@ -128,6 +128,14 @@ android {
}
}
+ debugMinify {
+ initWith debug
+ debuggable false
+ minifyEnabled true
+ shrinkResources true
+ matchingFallbacks = ['debug']
+ }
+
release {
signingConfig signingConfigs.release
debuggable false
@@ -240,6 +248,7 @@ android {
hilt {
enableTransformForLocalTests = true
enableAggregatingTask = false
+ enableExperimentalClasspathAggregation = true
}
}
@@ -350,6 +359,8 @@ dependencies {
implementation Libs.ROOM_COROUTINES
testImplementation Libs.HAMCREST
+
+ androidTestImplementation Libs.COMPOSE_UI_TEST
}
// Comment out this line if the reporting logic starts going wonky.
diff --git a/apps/student/flank.yml b/apps/student/flank.yml
index e6a449008d..45f1d8bccf 100644
--- a/apps/student/flank.yml
+++ b/apps/student/flank.yml
@@ -1,8 +1,8 @@
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: ./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
diff --git a/apps/student/flank_coverage.yml b/apps/student/flank_coverage.yml
index 67858de14e..3af12c3e0b 100644
--- a/apps/student/flank_coverage.yml
+++ b/apps/student/flank_coverage.yml
@@ -19,10 +19,10 @@ gcloud:
directories-to-pull:
- /sdcard/
test-targets:
- - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub
+ - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.OfflineE2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage
device:
- - model: NexusLowRes
- version: 26
+ - model: Pixel2.arm
+ version: 29
locale: en_US
orientation: portrait
diff --git a/apps/student/flank_e2e_coverage.yml b/apps/student/flank_e2e_coverage.yml
index ef7cccbfd2..55a9ea0a1b 100644
--- a/apps/student/flank_e2e_coverage.yml
+++ b/apps/student/flank_e2e_coverage.yml
@@ -19,11 +19,11 @@ gcloud:
directories-to-pull:
- /sdcard/
test-targets:
- - annotation com.instructure.canvas.espresso.E2E
- - notAnnotation com.instructure.canvas.espresso.Stub
+ - annotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.OfflineE2E
+ - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage
device:
- - model: Nexus6P
- version: 26
+ - model: Pixel2.arm
+ version: 29
locale: en_US
orientation: portrait
diff --git a/apps/student/flank_e2e_offline.yml b/apps/student/flank_e2e_offline.yml
index 4d76f14bd3..0f091632dd 100644
--- a/apps/student/flank_e2e_offline.yml
+++ b/apps/student/flank_e2e_offline.yml
@@ -21,6 +21,6 @@ gcloud:
orientation: portrait
flank:
- testShards: 1
+ testShards: 10
testRuns: 1
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt
index 3793f2be87..c61b83cc1e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt
@@ -24,24 +24,22 @@ import androidx.test.rule.GrantPermissionRule
import com.instructure.canvas.espresso.E2E
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.checkToastText
import com.instructure.dataseeding.api.AssignmentGroupsApi
import com.instructure.dataseeding.api.AssignmentsApi
import com.instructure.dataseeding.api.CoursesApi
import com.instructure.dataseeding.api.SubmissionsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.AttachmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.FileUploadType
import com.instructure.dataseeding.model.GradingType
-import com.instructure.dataseeding.model.SubmissionApiModel
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.ago
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
+import com.instructure.student.R
import com.instructure.student.ui.pages.AssignmentListPage
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.ViewUtils
@@ -54,6 +52,7 @@ import org.junit.Test
@HiltAndroidTest
class AssignmentsE2ETest: StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -65,6 +64,117 @@ class AssignmentsE2ETest: StudentTest() {
android.Manifest.permission.CAMERA
)
+ @E2E
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.E2E, SecondaryFeatureCategory.ASSIGNMENT_REMINDER)
+ fun testAssignmentReminderE2E() {
+
+ Log.d(PREPARATION_TAG,"Seeding data.")
+ val data = seedData(students = 1, teachers = 1, courses = 1)
+ val student = data.studentsList[0]
+ val teacher = data.teachersList[0]
+ val course = data.coursesList[0]
+
+ Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for '${course.name}' course with 2 days ahead due date.")
+ val testAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, dueAt = 2.days.fromNow.iso8601, pointsPossible = 15.0, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
+
+ Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for '${course.name}' course with 2 days past due date.")
+ val alreadyPastAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, dueAt = 2.days.ago.iso8601, pointsPossible = 15.0, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
+
+ Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.")
+ tokenLogin(student)
+ dashboardPage.waitForRender()
+
+ Log.d(STEP_TAG,"Select course: '${course.name}'.")
+ dashboardPage.selectCourse(course)
+
+ Log.d(STEP_TAG,"Navigate to course Assignments Page.")
+ courseBrowserPage.selectAssignments()
+
+ Log.d(STEP_TAG,"Click on assignment '${testAssignment.name}'.")
+ assignmentListPage.clickAssignment(testAssignment)
+
+ Log.d(STEP_TAG, "Assert that the corresponding views are displayed on the Assignment Details Page. Assert that the reminder section is displayed as well.")
+ assignmentDetailsPage.assertPageObjects()
+ assignmentDetailsPage.assertReminderSectionDisplayed()
+
+ Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.")
+ assignmentDetailsPage.clickAddReminder()
+
+ Log.d(STEP_TAG, "Select '1 Hour Before' and assert that the reminder has been picked up and displayed on the Assignment Details Page.")
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+ assignmentDetailsPage.assertReminderDisplayedWithText("1 Hour Before")
+
+ Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.")
+ assignmentDetailsPage.clickAddReminder()
+
+ Log.d(STEP_TAG, "Select '1 Hour Before' again, and assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice.")
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+ checkToastText(R.string.reminderAlreadySet, activityRule.activity)
+
+ Log.d(STEP_TAG, "Remove the '1 Hour Before' reminder, confirm the deletion dialog and assert that the '1 Hour Before' reminder is not displayed any more.")
+ assignmentDetailsPage.removeReminderWithText("1 Hour Before")
+ assignmentDetailsPage.assertReminderNotDisplayedWithText("1 Hour Before")
+
+ Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.")
+ assignmentDetailsPage.clickAddReminder()
+
+ Log.d(STEP_TAG, "Select 'Custom' reminder.")
+ assignmentDetailsPage.clickCustom()
+
+ Log.d(STEP_TAG, "Assert that the 'Done' button is disabled by default.")
+ assignmentDetailsPage.assertDoneButtonIsDisabled()
+
+ Log.d(STEP_TAG, "Fill the quantity text input with '15' and assert that the 'Done' button is still disabled since there is no option selected yet.")
+ assignmentDetailsPage.fillQuantity("15")
+ assignmentDetailsPage.assertDoneButtonIsDisabled()
+
+ Log.d(STEP_TAG, "Select the 'Hours Before' option, and click on 'Done' button, since it will be enabled because both the quantity and option are filled and selected.")
+ assignmentDetailsPage.clickHoursBefore()
+ assignmentDetailsPage.clickDone()
+
+ Log.d(STEP_TAG, "Assert that the '15 Hours Before' reminder is displayed on the Assignment Details Page.")
+ assignmentDetailsPage.assertReminderDisplayedWithText("15 Hours Before")
+
+ Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.")
+ assignmentDetailsPage.clickAddReminder()
+
+ Log.d(STEP_TAG, "Select '1 Week Before' and assert that a toast message is occurring which warns that we cannot pick up a reminder which has already passed (for example cannot pick '1 Week Before' reminder for an assignment which ends tomorrow).")
+ assignmentDetailsPage.selectTimeOption("1 Week Before")
+ assignmentDetailsPage.assertReminderNotDisplayedWithText("1 Week Before")
+ checkToastText(R.string.reminderInPast, activityRule.activity)
+
+ Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.")
+ assignmentDetailsPage.clickAddReminder()
+
+ Log.d(STEP_TAG, "Select '1 Day Before' and assert that the reminder has been picked up and displayed on the Assignment Details Page.")
+ assignmentDetailsPage.selectTimeOption("1 Day Before")
+ assignmentDetailsPage.assertReminderDisplayedWithText("1 Day Before")
+
+ Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.")
+ assignmentDetailsPage.clickAddReminder()
+
+ Log.d(STEP_TAG, "Select 'Custom' reminder.")
+ assignmentDetailsPage.clickCustom()
+
+ Log.d(STEP_TAG, "Fill the quantity text input with '24' and select 'Hours Before' as option. Click on 'Done'.")
+ assignmentDetailsPage.fillQuantity("24")
+ assignmentDetailsPage.clickHoursBefore()
+ assignmentDetailsPage.clickDone()
+
+ Log.d(STEP_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice. (Because 1 days and 24 hours is the same)")
+ checkToastText(R.string.reminderAlreadySet, activityRule.activity)
+
+ Log.d(STEP_TAG, "Navigate back to Assignment List Page.")
+ Espresso.pressBack()
+
+ Log.d(STEP_TAG,"Click on assignment '${alreadyPastAssignment.name}'.")
+ assignmentListPage.clickAssignment(alreadyPastAssignment)
+
+ Log.d(STEP_TAG, "Assert that the reminder section is NOT displayed, because the '${alreadyPastAssignment.name}' assignment has already passed..")
+ assignmentDetailsPage.assertReminderSectionNotDisplayed()
+ }
+
@E2E
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.E2E)
@@ -77,7 +187,14 @@ class AssignmentsE2ETest: StudentTest() {
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.")
- val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601)
+ val pointsTextAssignment = AssignmentsApi.createAssignment(
+ courseId = course.id,
+ teacherToken = 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)
@@ -106,14 +223,14 @@ class AssignmentsE2ETest: StudentTest() {
Espresso.pressBack()
Log.d(PREPARATION_TAG,"Submit assignment: ${pointsTextAssignment.name} for student: ${student.name}.")
- submitAssignment(pointsTextAssignment, course, student)
+ SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY)))
assignmentDetailsPage.refresh()
assignmentDetailsPage.assertStatusSubmitted()
assignmentDetailsPage.assertSubmissionAndRubricLabel()
Log.d(PREPARATION_TAG,"Grade submission: ${pointsTextAssignment.name} with 13 points.")
- val textGrade = gradeSubmission(teacher, course, pointsTextAssignment.id, student, "13")
+ val textGrade = SubmissionsApi.gradeSubmission(teacher.token, course.id, pointsTextAssignment.id, student.id, postedGrade = "13")
Log.d(STEP_TAG,"Refresh the page. Assert that the assignment ${pointsTextAssignment.name} has been graded with 13 points.")
assignmentDetailsPage.refresh()
@@ -137,13 +254,13 @@ class AssignmentsE2ETest: StudentTest() {
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.")
- val letterGradeTextAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0)
+ val letterGradeTextAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.LETTER_GRADE, pointsPossible = 20.0, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
Log.d(PREPARATION_TAG,"Submit assignment: ${letterGradeTextAssignment.name} for student: ${student.name}.")
- submitAssignment(letterGradeTextAssignment, course, student)
+ SubmissionsApi.seedAssignmentSubmission(course.id, student.token, letterGradeTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY)))
Log.d(PREPARATION_TAG,"Grade submission: ${letterGradeTextAssignment.name} with 13 points.")
- val submissionGrade = gradeSubmission(teacher, course, letterGradeTextAssignment.id, student, "13")
+ val submissionGrade = SubmissionsApi.gradeSubmission(teacher.token, course.id, letterGradeTextAssignment.id, student.id, postedGrade = "13")
Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.")
tokenLogin(student)
@@ -169,7 +286,7 @@ class AssignmentsE2ETest: StudentTest() {
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.")
- val percentageFileAssignment = createAssignment(course.id, teacher, GradingType.PERCENT, 25.0, allowedExtensions = listOf("txt", "pdf", "jpg"), submissionType = listOf(SubmissionType.ONLINE_UPLOAD))
+ val percentageFileAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.PERCENT, pointsPossible = 25.0, allowedExtensions = listOf("txt", "pdf", "jpg"), submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD))
Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.")
tokenLogin(student)
@@ -194,14 +311,14 @@ class AssignmentsE2ETest: StudentTest() {
)
Log.d(PREPARATION_TAG,"Submit ${percentageFileAssignment.name} assignment for ${student.name} student.")
- submitCourseAssignment(course, percentageFileAssignment, uploadInfo, student)
+ SubmissionsApi.submitCourseAssignment(course.id, student.token, percentageFileAssignment.id, SubmissionType.ONLINE_UPLOAD, fileIds = mutableListOf(uploadInfo.id))
Log.d(STEP_TAG,"Refresh the page. Assert that the ${percentageFileAssignment.name} assignment has been submitted.")
assignmentDetailsPage.refresh()
assignmentDetailsPage.assertAssignmentSubmitted()
Log.d(PREPARATION_TAG,"Grade ${percentageFileAssignment.name} assignment with 22 percentage.")
- gradeSubmission(teacher, course, percentageFileAssignment, student,"22")
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, percentageFileAssignment.id, student.id, postedGrade = "22")
Log.d(STEP_TAG,"Refresh the page. Assert that the ${percentageFileAssignment.name} assignment has been graded with 22 percentage.")
assignmentDetailsPage.refresh()
@@ -229,21 +346,6 @@ class AssignmentsE2ETest: StudentTest() {
submissionDetailsPage.assertFileDisplayed(uploadInfo.fileName)
}
- private fun submitCourseAssignment(
- course: CourseApiModel,
- percentageFileAssignment: AssignmentApiModel,
- uploadInfo: AttachmentApiModel,
- student: CanvasUserApiModel
- ) {
- SubmissionsApi.submitCourseAssignment(
- submissionType = SubmissionType.ONLINE_UPLOAD,
- courseId = course.id,
- assignmentId = percentageFileAssignment.id,
- fileIds = listOf(uploadInfo.id).toMutableList(),
- studentToken = student.token
- )
- }
-
@E2E
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.E2E)
@@ -255,36 +357,36 @@ class AssignmentsE2ETest: StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.")
- val letterGradeTextAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0)
+ Log.d(PREPARATION_TAG,"Seeding assignment for '${course.name}' course.")
+ val letterGradeTextAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.LETTER_GRADE, pointsPossible = 20.0, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
- Log.d(PREPARATION_TAG,"Submit ${letterGradeTextAssignment.name} assignment for ${student.name} student.")
- submitAssignment(letterGradeTextAssignment, course, student)
+ Log.d(PREPARATION_TAG,"Submit '${letterGradeTextAssignment.name}' assignment for '${student.name}' student.")
+ SubmissionsApi.seedAssignmentSubmission(course.id, student.token, letterGradeTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY)))
- Log.d(PREPARATION_TAG,"Grade ${letterGradeTextAssignment.name} assignment with 16.")
- gradeSubmission(teacher, course, letterGradeTextAssignment, student, "16")
+ Log.d(PREPARATION_TAG,"Grade '${letterGradeTextAssignment.name}' assignment with 16.")
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, letterGradeTextAssignment.id, student.id, postedGrade = "16")
- Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.")
- val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601)
+ Log.d(PREPARATION_TAG,"Seeding 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(PREPARATION_TAG,"Submit ${pointsTextAssignment.name} assignment for ${student.name} student.")
- submitAssignment(pointsTextAssignment, course, student)
+ Log.d(PREPARATION_TAG,"Submit '${pointsTextAssignment.name}' assignment for '${student.name}' student.")
+ SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY)))
- Log.d(PREPARATION_TAG,"Grade ${pointsTextAssignment.name} assignment with 13 points.")
- gradeSubmission(teacher, course, pointsTextAssignment.id, student, "13")
+ Log.d(PREPARATION_TAG,"Grade '${pointsTextAssignment.name}' assignment with 13 points.")
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, pointsTextAssignment.id, student.id, postedGrade = "13")
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 it's Assignments Page.")
+ Log.d(STEP_TAG,"Select '${course.name}' course and navigate to it's Assignments Page.")
dashboardPage.selectCourse(course)
courseBrowserPage.selectAssignments()
- Log.d(STEP_TAG,"Assert that ${pointsTextAssignment.name} assignment is displayed with the corresponding grade: 13.")
+ Log.d(STEP_TAG,"Assert that '${pointsTextAssignment.name}' assignment is displayed with the corresponding grade: 13.")
assignmentListPage.assertHasAssignment(pointsTextAssignment,"13")
- Log.d(STEP_TAG,"Assert that ${letterGradeTextAssignment.name} assignment is displayed with the corresponding grade: 16.")
+ Log.d(STEP_TAG,"Assert that '${letterGradeTextAssignment.name}' assignment is displayed with the corresponding grade: 16.")
assignmentListPage.assertHasAssignment(letterGradeTextAssignment, "16")
}
@@ -299,22 +401,22 @@ class AssignmentsE2ETest: StudentTest() {
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Seeding assignment for '${course.name}' course.")
- val upcomingAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0)
+ 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 assignment for '${course.name}' course.")
- val missingAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0, 2.days.ago.iso8601)
+ 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.")
- val gradedAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0)
+ 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.")
- gradeSubmission(teacher, course, gradedAssignment, student, "11")
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, gradedAssignment.id, student.id, postedGrade = "11")
Log.d(PREPARATION_TAG,"Create an Assignment Group for '${course.name}' course.")
- val assignmentGroup = createAssignmentGroup(teacher, course)
+ val assignmentGroup = AssignmentGroupsApi.createAssignmentGroup(teacher.token, course.id, name = "Discussions")
Log.d(PREPARATION_TAG,"Seeding assignment for '${course.name}' course.")
- val otherTypeAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0, assignmentGroupId = assignmentGroup.id)
+ 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}.")
tokenLogin(student)
@@ -407,18 +509,6 @@ class AssignmentsE2ETest: StudentTest() {
assignmentListPage.assertAssignmentNotDisplayed(gradedAssignment.name)
}
- private fun createAssignmentGroup(
- teacher: CanvasUserApiModel,
- course: CourseApiModel
- ) = AssignmentGroupsApi.createAssignmentGroup(
- token = teacher.token,
- courseId = course.id,
- name = "Discussions",
- position = null,
- groupWeight = null,
- sisSourceId = null
- )
-
@E2E
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.COMMENTS, TestCategory.E2E)
@@ -430,21 +520,21 @@ class AssignmentsE2ETest: StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.")
- val assignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601)
+ Log.d(PREPARATION_TAG,"Seeding assignment for '${course.name}' course.")
+ val assignment = 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(PREPARATION_TAG,"Submit ${assignment.name} assignment for ${student.name} student.")
- submitAssignment(assignment, course, student)
+ Log.d(PREPARATION_TAG,"Submit '${assignment.name}' assignment for '${student.name}' student.")
+ SubmissionsApi.seedAssignmentSubmission(course.id, student.token, assignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY)))
- Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.")
+ 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 it's Assignments Page.")
+ Log.d(STEP_TAG,"Select '${course.name}' course and navigate to it's Assignments Page.")
dashboardPage.selectCourse(course)
courseBrowserPage.selectAssignments()
- Log.d(STEP_TAG,"Click on ${assignment.name} assignment.")
+ Log.d(STEP_TAG,"Click on '${assignment.name}' assignment.")
assignmentListPage.clickAssignment(assignment)
Log.d(STEP_TAG,"Navigate to submission details Comments Tab.")
@@ -474,13 +564,13 @@ class AssignmentsE2ETest: StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.")
- val assignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601)
+ Log.d(PREPARATION_TAG,"Seeding assignment for '${course.name}' course.")
+ val assignment = 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(PREPARATION_TAG,"Submit ${assignment.name} assignment for ${student.name} student.")
- submitAssignment(assignment, course, student)
+ Log.d(PREPARATION_TAG,"Submit '${assignment.name}' assignment for '${student.name}' student.")
+ SubmissionsApi.seedAssignmentSubmission(course.id, student.token, assignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY)))
- Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
dashboardPage.waitForRender()
@@ -491,16 +581,16 @@ class AssignmentsE2ETest: StudentTest() {
token = student.token,
fileUploadType = FileUploadType.COMMENT_ATTACHMENT
)
- commentOnSubmission(student, course, assignment, commentUploadInfo)
+ SubmissionsApi.commentOnSubmission(course.id, student.token, assignment.id, mutableListOf(commentUploadInfo.id))
- Log.d(STEP_TAG,"Select ${course.name} course and navigate to it's Assignments Page.")
+ Log.d(STEP_TAG,"Select '${course.name}' course and navigate to it's Assignments Page.")
dashboardPage.selectCourse(course)
courseBrowserPage.selectAssignments()
- Log.d(STEP_TAG,"Click on ${assignment.name} assignment.")
+ Log.d(STEP_TAG,"Click on '${assignment.name}' assignment.")
assignmentListPage.clickAssignment(assignment)
- Log.d(STEP_TAG,"Assert that ${commentUploadInfo.fileName} file is displayed as a comment by ${student.name} student.")
+ Log.d(STEP_TAG,"Assert that '${commentUploadInfo.fileName}' file is displayed as a comment by '${student.name}' student.")
assignmentDetailsPage.goToSubmissionDetails()
submissionDetailsPage.openComments()
submissionDetailsPage.assertCommentAttachmentDisplayed(commentUploadInfo.fileName, student)
@@ -520,26 +610,26 @@ class AssignmentsE2ETest: StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.")
- val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601)
+ 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}.")
+ 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}.")
+ 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, "Verify that our assignments are present," +
- "along with any grade/date info. Click on assignment ${pointsTextAssignment.name}.")
+ "along with any grade/date info. Click on assignment '${pointsTextAssignment.name}'.")
assignmentListPage.assertHasAssignment(pointsTextAssignment)
assignmentListPage.clickAssignment(pointsTextAssignment)
- Log.d(PREPARATION_TAG,"Submit assignment: ${pointsTextAssignment.name} for student: ${student.name}.")
- submitAssignment(pointsTextAssignment, course, student)
+ 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(STEP_TAG, "Refresh the page.")
assignmentDetailsPage.refresh()
@@ -547,8 +637,8 @@ class AssignmentsE2ETest: StudentTest() {
Log.d(STEP_TAG, "Assert that when only there is one attempt, the spinner is not displayed.")
assignmentDetailsPage.assertNoAttemptSpinner()
- Log.d(PREPARATION_TAG,"Generate another submission for assignment: ${pointsTextAssignment.name} for student: ${student.name}.")
- submitAssignment(pointsTextAssignment, course, student)
+ Log.d(PREPARATION_TAG,"Generate another submission for 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(STEP_TAG, "Refresh the page.")
assignmentDetailsPage.refresh()
@@ -582,20 +672,20 @@ class AssignmentsE2ETest: StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.")
- val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601)
+ 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}.")
+ 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}.")
+ 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,"Verify that our assignments are present, along with any grade/date info. Click on assignment ${pointsTextAssignment.name}.")
+ Log.d(STEP_TAG,"Verify that our assignments are present, along with any grade/date info. Click on assignment '${pointsTextAssignment.name}'.")
assignmentListPage.assertHasAssignment(pointsTextAssignment)
assignmentListPage.clickAssignment(pointsTextAssignment)
@@ -611,16 +701,16 @@ class AssignmentsE2ETest: StudentTest() {
submissionDetailsPage.assertNoSubmissionEmptyView()
Espresso.pressBack()
- Log.d(PREPARATION_TAG,"Submit assignment: ${pointsTextAssignment.name} for student: ${student.name}.")
- submitAssignment(pointsTextAssignment, course, student)
+ 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(STEP_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.")
assignmentDetailsPage.refresh()
assignmentDetailsPage.assertStatusSubmitted()
assignmentDetailsPage.assertSubmissionAndRubricLabel()
- Log.d(PREPARATION_TAG,"Make another submission for assignment: ${pointsTextAssignment.name} for student: ${student.name}.")
- val secondSubmissionAttempt = submitAssignment(pointsTextAssignment, course, student)
+ 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)))
Log.d(STEP_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.")
assignmentDetailsPage.refresh()
@@ -635,7 +725,7 @@ class AssignmentsE2ETest: StudentTest() {
assignmentDetailsPage.goToSubmissionDetails()
submissionDetailsPage.openComments()
- Log.d(STEP_TAG,"Assert that ${secondSubmissionAttempt[0].body} text submission has been displayed as a comment.")
+ Log.d(STEP_TAG,"Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.")
submissionDetailsPage.assertTextSubmissionDisplayedAsComment()
val newComment = "Comment for second attempt"
@@ -676,21 +766,21 @@ class AssignmentsE2ETest: StudentTest() {
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.")
- val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601)
+ 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.")
- gradeSubmission(teacher, course, pointsTextAssignment.id, student, "12")
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, pointsTextAssignment.id, student.id, postedGrade = "12")
Log.d(STEP_TAG, "Refresh the Dashboard page. Assert that the course grade is 80%.")
dashboardPage.refresh()
dashboardPage.assertCourseGrade(course.name, "80%")
Log.d(PREPARATION_TAG, "Update ${course.name} course's settings: Enable restriction for quantitative data.")
- var restrictQuantitativeDataMap = mutableMapOf()
+ val restrictQuantitativeDataMap = mutableMapOf()
restrictQuantitativeDataMap["restrict_quantitative_data"] = true
CoursesApi.updateCourseSettings(course.id, restrictQuantitativeDataMap)
@@ -699,28 +789,28 @@ class AssignmentsE2ETest: StudentTest() {
dashboardPage.assertCourseGrade(course.name, "B-")
Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.")
- val percentageAssignment = createAssignment(course.id, teacher, GradingType.PERCENT, 15.0, 1.days.fromNow.iso8601)
+ 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).")
- gradeSubmission(teacher, course, percentageAssignment.id, student, "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 = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 15.0, 1.days.fromNow.iso8601)
+ 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.")
- gradeSubmission(teacher, course, letterGradeAssignment.id, student, "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 = createAssignment(course.id, teacher, GradingType.PASS_FAIL, 15.0, 1.days.fromNow.iso8601)
+ 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'.")
- gradeSubmission(teacher, course, passFailAssignment.id, student, "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 = createAssignment(course.id, teacher, GradingType.GPA_SCALE, 15.0, 1.days.fromNow.iso8601)
+ 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.")
- gradeSubmission(teacher, course, gpaScaleAssignment.id, student, "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()
@@ -831,18 +921,18 @@ class AssignmentsE2ETest: StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.")
- val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601)
+ 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}.")
+ 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.")
- gradeSubmission(teacher, course, pointsTextAssignment.id, student, "12")
+ 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(PREPARATION_TAG, "Update ${course.name} course's settings: Enable restriction for quantitative data.")
- var restrictQuantitativeDataMap = mutableMapOf()
+ 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)
@@ -850,52 +940,34 @@ class AssignmentsE2ETest: StudentTest() {
dashboardPage.refresh()
dashboardPage.assertCourseGrade(course.name, "B-")
- Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.")
- val percentageAssignment = createAssignment(course.id, teacher, GradingType.PERCENT, 15.0, 1.days.fromNow.iso8601)
+ 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).")
- gradeSubmission(teacher, course, percentageAssignment.id, student, "10")
+ 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 = createAssignment(
- course.id,
- teacher,
- GradingType.LETTER_GRADE,
- 15.0,
- 1.days.fromNow.iso8601
- )
+ 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.")
- gradeSubmission(teacher, course, letterGradeAssignment.id, student, "C")
+ 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 = createAssignment(
- course.id,
- teacher,
- GradingType.PASS_FAIL,
- 15.0,
- 1.days.fromNow.iso8601
- )
+ 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'.")
- gradeSubmission(teacher, course, passFailAssignment.id, student, "Incomplete")
+ 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 = createAssignment(
- course.id,
- teacher,
- GradingType.GPA_SCALE,
- 15.0,
- 1.days.fromNow.iso8601
- )
+ 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.")
- gradeSubmission(teacher, course, gpaScaleAssignment.id, student, "3.7")
+ 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.")
+ Log.d(STEP_TAG, "Select course: '${course.name}'. Select 'Grades' menu.")
dashboardPage.selectCourse(course)
courseBrowserPage.selectGrades()
@@ -908,7 +980,7 @@ class AssignmentsE2ETest: StudentTest() {
if(isLowResDevice()) courseGradesPage.swipeUp()
courseGradesPage.assertAssignmentDisplayed(gpaScaleAssignment.name, "F")
- Log.d(PREPARATION_TAG, "Update ${course.name} course's settings: Enable restriction for quantitative data.")
+ 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)
@@ -925,95 +997,4 @@ class AssignmentsE2ETest: StudentTest() {
courseGradesPage.swipeUp()
courseGradesPage.assertAssignmentDisplayed(gpaScaleAssignment.name, "3.7/15 (F)")
}
-
- private fun createAssignment(
- courseId: Long,
- teacher: CanvasUserApiModel,
- gradingType: GradingType,
- pointsPossible: Double,
- dueAt: String = EMPTY_STRING,
- allowedExtensions: List? = null,
- assignmentGroupId: Long? = null,
- submissionType: List = listOf(SubmissionType.ONLINE_TEXT_ENTRY)
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = courseId,
- submissionTypes = submissionType,
- gradingType = gradingType,
- teacherToken = teacher.token,
- pointsPossible = pointsPossible,
- dueAt = dueAt,
- allowedExtensions = allowedExtensions,
- assignmentGroupId = assignmentGroupId
- )
- )
- }
-
- private fun submitAssignment(
- assignment: AssignmentApiModel,
- course: CourseApiModel,
- student: CanvasUserApiModel
- ): List {
- return SubmissionsApi.seedAssignmentSubmission(
- SubmissionsApi.SubmissionSeedRequest(
- assignmentId = assignment.id,
- courseId = course.id,
- studentToken = student.token,
- submissionSeedsList = listOf(
- SubmissionsApi.SubmissionSeedInfo(
- amount = 1,
- submissionType = SubmissionType.ONLINE_TEXT_ENTRY
- )
- )
- )
- )
- }
-
- private fun gradeSubmission(
- teacher: CanvasUserApiModel,
- course: CourseApiModel,
- assignment: AssignmentApiModel,
- student: CanvasUserApiModel,
- postedGrade: String,
- excused: Boolean = false
- ) {
- SubmissionsApi.gradeSubmission(
- teacherToken = teacher.token,
- courseId = course.id,
- assignmentId = assignment.id,
- studentId = student.id,
- postedGrade = postedGrade,
- excused = excused
- )
- }
-
- private fun gradeSubmission(
- teacher: CanvasUserApiModel,
- course: CourseApiModel,
- assignmentId: Long,
- student: CanvasUserApiModel,
- postedGrade: String
- ) = SubmissionsApi.gradeSubmission(
- teacherToken = teacher.token,
- courseId = course.id,
- assignmentId = assignmentId,
- studentId = student.id,
- postedGrade = postedGrade,
- excused = false
- )
-
- private fun commentOnSubmission(
- student: CanvasUserApiModel,
- course: CourseApiModel,
- assignment: AssignmentApiModel,
- commentUploadInfo: AttachmentApiModel
- ) {
- SubmissionsApi.commentOnSubmission(
- studentToken = student.token,
- courseId = course.id,
- assignmentId = assignment.id,
- fileIds = mutableListOf(commentUploadInfo.id)
- )
- }
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt
index 5a70977edd..e9429327ce 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt
@@ -25,9 +25,6 @@ import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvas.espresso.refresh
import com.instructure.dataseeding.api.AssignmentsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.GradingType
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.days
@@ -58,7 +55,7 @@ class BookmarksE2ETest : StudentTest() {
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Preparing an assignment which will be saved as a bookmark.")
- val assignment = createAssignment(course, teacher)
+ val assignment = 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)
@@ -110,20 +107,4 @@ class BookmarksE2ETest : StudentTest() {
bookmarkPage.assertEmptyView()
}
- private fun createAssignment(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = course.id,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- gradingType = GradingType.POINTS,
- teacherToken = teacher.token,
- pointsPossible = 15.0,
- dueAt = 1.days.fromNow.iso8601
- )
- )
- }
-
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt
index 69fd95887e..6b22a2228c 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt
@@ -3,7 +3,6 @@ package com.instructure.student.ui.e2e
import android.util.Log
import com.instructure.canvas.espresso.E2E
import com.instructure.canvas.espresso.FeatureCategory
-import com.instructure.canvas.espresso.KnownBug
import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
@@ -15,20 +14,15 @@ import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
-/**
- * Very basic test to verify that the collaborations web page shows up correctly.
- * We make no attempt to actually start a collaboration.
- * This test could break if changes are made to the web page that we bring up.
- */
@HiltAndroidTest
class CollaborationsE2ETest: StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@E2E
@Test
- @KnownBug("https://instructure.atlassian.net/browse/VICE-3157")
@TestMetaData(Priority.MANDATORY, FeatureCategory.COLLABORATIONS, TestCategory.E2E)
fun testCollaborationsE2E() {
@@ -37,24 +31,24 @@ class CollaborationsE2ETest: StudentTest() {
val student = data.studentsList[0]
val course = data.coursesList[0]
- Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG,"Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
dashboardPage.waitForRender()
- Log.d(STEP_TAG,"Navigate to ${course.name} course's Collaborations Page.")
+ Log.d(STEP_TAG,"Navigate to '${course.name}' course's Collaborations Page.")
dashboardPage.selectCourse(course)
courseBrowserPage.selectCollaborations()
Log.d(STEP_TAG,"Verify that various elements of the web page are present.")
CollaborationsPage.assertCurrentCollaborationsHeaderPresent()
- //On some screen size, this spinner does not displayed at all, instead of it,
- //there is a button on the top-right corner with the 'Start a new Collaboration' text
- //and clicking on it will 'expand' and display this spinner.
- //However, there is a bug (see link in this @KnownBug annotation) which is about the button not displayed on some screen size
- //So this test will breaks until it this ticket will be fixed.
+ Log.d(STEP_TAG, "Assert that the 'Start a New Collaboration' button is displayed.")
CollaborationsPage.assertStartANewCollaborationPresent()
- CollaborationsPage.assertGoogleDocsChoicePresent()
- CollaborationsPage.assertGoogleDocsExplanationPresent()
+
+ Log.d(STEP_TAG, "Assert that within the selector, the 'Google Docs' has been selected as the default value.")
+ CollaborationsPage.assertGoogleDocsChoicePresentAsDefaultOption()
+
+ Log.d(STEP_TAG, "Assert that the warning section (under the selector) of Google Docs has been displayed.")
+ CollaborationsPage.assertGoogleDocsWarningDescriptionPresent()
}
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt
index dcc641e94d..b79cfa0ae7 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt
@@ -53,15 +53,11 @@ class ConferencesE2ETest: StudentTest() {
val testConferenceTitle = "E2E test conference"
val testConferenceDescription = "Nightly E2E Test conference description"
Log.d(PREPARATION_TAG,"Create a conference with '$testConferenceTitle' title and '$testConferenceDescription' description.")
- ConferencesApi.createCourseConference(teacher.token,
- testConferenceTitle, testConferenceDescription,"BigBlueButton",false,70,
- listOf(student.id),course.id)
+ ConferencesApi.createCourseConference(course.id, teacher.token, testConferenceTitle, testConferenceDescription, recipientUserIds = listOf(student.id))
val testConferenceTitle2 = "E2E test conference 2"
val testConferenceDescription2 = "Nightly E2E Test conference description 2"
- ConferencesApi.createCourseConference(teacher.token,
- testConferenceTitle2, testConferenceDescription2,"BigBlueButton",true,120,
- listOf(student.id),course.id)
+ ConferencesApi.createCourseConference(course.id, teacher.token, testConferenceTitle2, testConferenceDescription2, longRunning = true, duration = 120, recipientUserIds = listOf(student.id))
Log.d(STEP_TAG,"Refresh the page. Assert that $testConferenceTitle conference is displayed on the Conference List Page with the corresponding status.")
refresh()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt
index 9770e01244..4b11b77837 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt
@@ -50,10 +50,7 @@ class DashboardE2ETest : StudentTest() {
val course2 = data.coursesList[1]
Log.d(PREPARATION_TAG, "Seed an Inbox conversation via API.")
- ConversationsApi.createConversation(
- token = teacher.token,
- recipients = listOf(student.id.toString())
- )
+ ConversationsApi.createConversation(teacher.token, listOf(student.id.toString()))
Log.d(PREPARATION_TAG,"Seed some group info.")
val groupCategory = GroupsApi.createCourseGroupCategory(data.coursesList[0].id, teacher.token)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt
index bf6bbeca3b..aa5efe83d0 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt
@@ -25,8 +25,6 @@ import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.dataseeding.api.DiscussionTopicsApi
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.espresso.ViewUtils
import com.instructure.espresso.getCurrentDateInCanvasFormat
import com.instructure.student.ui.utils.StudentTest
@@ -52,51 +50,51 @@ class DiscussionsE2ETest: StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seed a discussion topic.")
- val topic1 = createDiscussion(course, teacher)
+ Log.d(PREPARATION_TAG,"Seed a discussion topic for '${course.name}' course.")
+ val topic1 = DiscussionTopicsApi.createDiscussion(course.id, teacher.token)
- Log.d(PREPARATION_TAG,"Seed another discussion topic.")
- val topic2 = createDiscussion(course, teacher)
+ Log.d(PREPARATION_TAG,"Seed another discussion topic for '${course.name}' course.")
+ val topic2 = DiscussionTopicsApi.createDiscussion(course.id, teacher.token)
- Log.d(STEP_TAG,"Seed an announcement for ${course.name} course.")
- val announcement = createAnnouncement(course, teacher)
+ Log.d(STEP_TAG,"Seed an announcement for '${course.name}' course.")
+ val announcement = DiscussionTopicsApi.createAnnouncement(course.id, teacher.token)
- Log.d(STEP_TAG,"Seed another announcement for ${course.name} course.")
- val announcement2 = createAnnouncement(course, teacher)
+ Log.d(STEP_TAG,"Seed another announcement for '${course.name}' course.")
+ val announcement2 = DiscussionTopicsApi.createAnnouncement(course.id, teacher.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}.")
+ Log.d(STEP_TAG,"Wait for the Dashboard Page to be rendered. Select course: '${course.name}'.")
+ dashboardPage.waitForRender()
dashboardPage.selectCourse(course)
- Log.d(STEP_TAG,"Verify that the Discussions and Announcements Tabs are both displayed on the CourseBrowser Page.")
+ Log.d(STEP_TAG,"Verify that the 'Discussions' and 'Announcements' Tabs are both displayed on the CourseBrowser Page.")
courseBrowserPage.assertTabDisplayed("Announcements")
courseBrowserPage.assertTabDisplayed("Discussions")
- Log.d(STEP_TAG,"Navigate to Announcements Page. Assert that both ${announcement.title} and ${announcement2.title} announcements are displayed.")
+ Log.d(STEP_TAG,"Navigate to Announcements Page. Assert that both '${announcement.title}' and '${announcement2.title}' announcements are displayed.")
courseBrowserPage.selectAnnouncements()
discussionListPage.assertTopicDisplayed(announcement.title)
discussionListPage.assertTopicDisplayed(announcement2.title)
- Log.d(STEP_TAG,"Select ${announcement.title} announcement and assert if the details page is displayed.")
+ Log.d(STEP_TAG,"Select '${announcement.title}' announcement and assert if the Discussion Details Page is displayed.")
discussionListPage.selectTopic(announcement.title)
discussionDetailsPage.assertTitleText(announcement.title)
- Log.d(STEP_TAG,"Navigate back.")
+ Log.d(STEP_TAG,"Navigate back to the Discussion List Page.")
Espresso.pressBack()
- Log.d(STEP_TAG,"Click on the 'Search' button and search for ${announcement2.title}. announcement.")
+ Log.d(STEP_TAG,"Click on the 'Search' button and search for '${announcement2.title}'. announcement.")
discussionListPage.searchable.clickOnSearchButton()
discussionListPage.searchable.typeToSearchBar(announcement2.title)
- Log.d(STEP_TAG,"Refresh the page. Assert that the searching method is working well, so ${announcement.title} won't be displayed and ${announcement2.title} is displayed.")
+ Log.d(STEP_TAG,"Refresh the page. Assert that the searching method is working well, so '${announcement.title}' won't be displayed and '${announcement2.title}' is displayed.")
discussionListPage.pullToUpdate()
discussionListPage.assertTopicDisplayed(announcement2.title)
discussionListPage.assertTopicNotDisplayed(announcement.title)
- Log.d(STEP_TAG,"Clear the search input field and assert that both announcements, ${announcement.title} and ${announcement2.title} has been diplayed.")
+ Log.d(STEP_TAG,"Clear the search input field and assert that both announcements, '${announcement.title}' and '${announcement2.title}' has been displayed.")
discussionListPage.searchable.clickOnClearSearchButton()
discussionListPage.waitForDiscussionTopicToDisplay(announcement.title)
discussionListPage.assertTopicDisplayed(announcement2.title)
@@ -107,22 +105,22 @@ class DiscussionsE2ETest: StudentTest() {
Log.d(STEP_TAG,"Navigate to Discussions Page.")
courseBrowserPage.selectDiscussions()
- Log.d(STEP_TAG,"Select ${topic1.title} discussion and assert if the details page is displayed and there is no reply for the discussion yet.")
+ Log.d(STEP_TAG,"Select '${topic1.title}' discussion and assert if the details page is displayed and there is no reply for the discussion yet.")
discussionListPage.assertTopicDisplayed(topic1.title)
discussionListPage.selectTopic(topic1.title)
discussionDetailsPage.assertTitleText(topic1.title)
discussionDetailsPage.assertNoRepliesDisplayed()
- Log.d(STEP_TAG,"Navigate back to Discussions Page.")
- Espresso.pressBack() // Back to discussion list
+ Log.d(STEP_TAG,"Navigate back to Discussion List Page.")
+ Espresso.pressBack()
- Log.d(STEP_TAG,"Select ${topic1.title} discussion and assert if the details page is displayed and there is no reply for the discussion yet.")
+ Log.d(STEP_TAG,"Select '${topic1.title}' discussion and assert if the details page is displayed and there is no reply for the discussion yet.")
discussionListPage.assertTopicDisplayed(topic2.title)
discussionListPage.selectTopic(topic2.title)
discussionDetailsPage.assertTitleText(topic2.title)
discussionDetailsPage.assertNoRepliesDisplayed()
- Log.d(STEP_TAG,"Navigate back to Discussions Page.")
+ Log.d(STEP_TAG,"Navigate back to Discussion List Page.")
Espresso.pressBack()
val newTopicName = "Do we really need discussions?"
@@ -131,11 +129,11 @@ class DiscussionsE2ETest: StudentTest() {
discussionListPage.createDiscussionTopic(newTopicName, newTopicDescription)
sleep(2000) // Allow some time for creation to propagate
- Log.d(STEP_TAG,"Assert that $newTopicName topic has been created successfully with 0 reply count.")
+ Log.d(STEP_TAG,"Assert that '$newTopicName' topic has been created successfully with 0 reply count.")
discussionListPage.assertTopicDisplayed(newTopicName)
discussionListPage.assertReplyCount(newTopicName, 0)
- Log.d(STEP_TAG,"Select $newTopicName topic and assert that there is no reply on the details page as well.")
+ Log.d(STEP_TAG,"Select '$newTopicName' topic and assert that there is no reply on the details page as well.")
discussionListPage.selectTopic(newTopicName)
discussionDetailsPage.assertNoRepliesDisplayed()
@@ -147,7 +145,7 @@ class DiscussionsE2ETest: StudentTest() {
Log.d(STEP_TAG,"Assert the the previously sent reply ($replyMessage) is displayed on the details page.")
discussionDetailsPage.assertRepliesDisplayed()
- Log.d(STEP_TAG,"Navigate back to Discussions Page.")
+ Log.d(STEP_TAG,"Navigate back to Discussion List Page.")
Espresso.pressBack()
Log.d(STEP_TAG,"Refresh the page. Assert that the previously sent reply has been counted, and there are no unread replies.")
@@ -158,22 +156,6 @@ class DiscussionsE2ETest: StudentTest() {
Log.d(STEP_TAG, "Assert that the due date is the current date (in the expected format).")
val currentDate = getCurrentDateInCanvasFormat()
discussionListPage.assertDueDate(newTopicName, currentDate)
-
}
- private fun createAnnouncement(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) = DiscussionTopicsApi.createAnnouncement(
- courseId = course.id,
- token = teacher.token
- )
-
- private fun createDiscussion(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) = DiscussionTopicsApi.createDiscussion(
- courseId = course.id,
- token = teacher.token
- )
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt
index b3cdea4a7b..15c3c673a3 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt
@@ -26,20 +26,19 @@ import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvasapi2.managers.DiscussionManager
import com.instructure.canvasapi2.models.CanvasContext
-import com.instructure.canvasapi2.models.DiscussionEntry
import com.instructure.canvasapi2.utils.weave.awaitApiResponse
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryWeave
import com.instructure.dataseeding.api.AssignmentsApi
import com.instructure.dataseeding.api.DiscussionTopicsApi
import com.instructure.dataseeding.api.SubmissionsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.AttachmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.FileUploadType
+import com.instructure.dataseeding.model.GradingType
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.Randomizer
+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.student.ui.utils.ViewUtils
import com.instructure.student.ui.utils.seedData
@@ -52,6 +51,7 @@ import java.io.FileWriter
@HiltAndroidTest
class FilesE2ETest: StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -67,8 +67,8 @@ class FilesE2ETest: StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.")
- val assignment = createAssignment(course, teacher)
+ Log.d(PREPARATION_TAG,"Seeding assignment for '${course.name}' course.")
+ val assignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), allowedExtensions = listOf("txt"))
Log.d(PREPARATION_TAG, "Seed a text file.")
val submissionUploadInfo = uploadTextFile(
@@ -78,20 +78,20 @@ class FilesE2ETest: StudentTest() {
fileUploadType = FileUploadType.ASSIGNMENT_SUBMISSION
)
- Log.d(PREPARATION_TAG,"Submit ${assignment.name} assignment for ${student.name} student.")
- submitAssignment(course, assignment, submissionUploadInfo, student)
+ Log.d(PREPARATION_TAG,"Submit '${assignment.name}' assignment for '${student.name}' student.")
+ SubmissionsApi.submitCourseAssignment(course.id, student.token, assignment.id, SubmissionType.ONLINE_UPLOAD, fileIds = mutableListOf(submissionUploadInfo.id))
- Log.d(STEP_TAG,"Seed a comment attachment upload.")
+ Log.d(STEP_TAG,"Seed a comment attachment (file) upload.")
val commentUploadInfo = uploadTextFile(
assignmentId = assignment.id,
courseId = course.id,
token = student.token,
fileUploadType = FileUploadType.COMMENT_ATTACHMENT
)
- commentOnSubmission(student, course, assignment, commentUploadInfo)
+ SubmissionsApi.commentOnSubmission(course.id, student.token, assignment.id, mutableListOf(commentUploadInfo.id))
- Log.d(STEP_TAG,"Seed a discussion for ${course.name} course.")
- val discussionTopic = createDiscussion(course, student)
+ Log.d(STEP_TAG,"Seed a discussion for '${course.name}' course.")
+ val discussionTopic = DiscussionTopicsApi.createDiscussion(course.id, student.token)
Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
tokenLogin(student)
@@ -102,7 +102,7 @@ class FilesE2ETest: StudentTest() {
Randomizer.randomTextFileName(Environment.getExternalStorageDirectory().absolutePath))
.apply { createNewFile() }
- Log.d(STEP_TAG,"Add some random content to the ${discussionAttachmentFile.name} file.")
+ Log.d(STEP_TAG,"Add some random content to the '${discussionAttachmentFile.name}' file.")
FileWriter(discussionAttachmentFile, true).apply {
write(Randomizer.randomTextFileContents())
flush()
@@ -111,7 +111,7 @@ class FilesE2ETest: StudentTest() {
Log.d(PREPARATION_TAG,"Use real API (rather than seeding) to create a reply to our discussion that contains an attachment.")
tryWeave {
- awaitApiResponse {
+ awaitApiResponse {
DiscussionManager.postToDiscussionTopic(
canvasContext = CanvasContext.emptyCourseContext(id = course.id),
topicId = discussionTopic.id,
@@ -124,22 +124,22 @@ class FilesE2ETest: StudentTest() {
Log.v(PREPARATION_TAG, "Discussion post error: $it")
}
- Log.d(STEP_TAG,"Navigate to 'Files' menu in user left-side menubar.")
+ Log.d(STEP_TAG,"Navigate to 'Files' menu in user left-side menu bar.")
leftSideNavigationDrawerPage.clickFilesMenu()
Log.d(STEP_TAG,"Assert that there is a directory called 'Submissions' is displayed.")
fileListPage.assertItemDisplayed("Submissions")
- Log.d(STEP_TAG,"Select 'Submissions' directory. Assert that ${discussionAttachmentFile.name} file is displayed on the File List Page.")
+ Log.d(STEP_TAG,"Select 'Submissions' directory. Assert that '${discussionAttachmentFile.name}' file is displayed on the File List Page.")
fileListPage.selectItem("Submissions")
- Log.d(STEP_TAG,"Assert that ${course.name} course is displayed.")
+ Log.d(STEP_TAG,"Assert that '${course.name}' course is displayed.")
fileListPage.assertItemDisplayed(course.name)
- Log.d(STEP_TAG,"Select ${course.name} course.")
+ Log.d(STEP_TAG,"Select '${course.name}' course.")
fileListPage.selectItem(course.name)
- Log.d(STEP_TAG,"Assert that ${discussionAttachmentFile.name} file is displayed on the File List Page.")
+ Log.d(STEP_TAG,"Assert that '${discussionAttachmentFile.name}' file is displayed on the File List Page.")
fileListPage.assertItemDisplayed(submissionUploadInfo.fileName)
Log.d(STEP_TAG,"Navigate back to File List Page.")
@@ -148,37 +148,37 @@ class FilesE2ETest: StudentTest() {
Log.d(STEP_TAG,"Assert that there is a directory called 'unfiled' is displayed.")
fileListPage.assertItemDisplayed("unfiled") // Our discussion attachment goes under "unfiled"
- Log.d(STEP_TAG,"Select 'unfiled' directory. Assert that ${discussionAttachmentFile.name} file is displayed on the File List Page.")
+ Log.d(STEP_TAG,"Select 'unfiled' directory. Assert that '${discussionAttachmentFile.name}' file is displayed on the File List Page.")
fileListPage.selectItem("unfiled")
fileListPage.assertItemDisplayed(discussionAttachmentFile.name)
Log.d(STEP_TAG,"Navigate back to Dashboard Page.")
ViewUtils.pressBackButton(2)
- Log.d(STEP_TAG,"Select ${course.name} course.")
+ Log.d(STEP_TAG,"Select '${course.name}' course.")
dashboardPage.selectCourse(course)
Log.d(STEP_TAG,"Navigate to Assignments Page.")
courseBrowserPage.selectAssignments()
- Log.d(STEP_TAG,"Click on ${assignment.name} assignment.")
+ Log.d(STEP_TAG,"Click on '${assignment.name}' assignment.")
assignmentListPage.clickAssignment(assignment)
Log.d(STEP_TAG,"Navigate to Submission Details Page and open Files Tab.")
assignmentDetailsPage.goToSubmissionDetails()
submissionDetailsPage.openFiles()
- Log.d(STEP_TAG,"Assert that ${submissionUploadInfo.fileName} file has been displayed.")
+ Log.d(STEP_TAG,"Assert that '${submissionUploadInfo.fileName}' file has been displayed.")
submissionDetailsPage.assertFileDisplayed(submissionUploadInfo.fileName)
- Log.d(STEP_TAG,"Open Comments Tab. Assert that ${commentUploadInfo.fileName} file is displayed as a comment by ${student.name} student.")
+ Log.d(STEP_TAG,"Open Comments Tab. Assert that '${commentUploadInfo.fileName}' file is displayed as a comment by '${student.name}' student.")
submissionDetailsPage.openComments()
submissionDetailsPage.assertCommentAttachmentDisplayed(commentUploadInfo.fileName, student)
Log.d(STEP_TAG,"Navigate back to Dashboard Page.")
ViewUtils.pressBackButton(4)
- Log.d(STEP_TAG,"Navigate to 'Files' menu in user left-side menubar.")
+ Log.d(STEP_TAG,"Navigate to 'Files' menu in user left-side menu bar.")
leftSideNavigationDrawerPage.clickFilesMenu()
Log.d(STEP_TAG,"Assert that there is a directory called 'unfiled' is displayed.")
@@ -194,18 +194,18 @@ class FilesE2ETest: StudentTest() {
fileListPage.assertItemNotDisplayed("unfiled")
fileListPage.searchable.pressSearchBackButton()
- Log.d(STEP_TAG,"Select 'unfiled' directory. Assert that ${discussionAttachmentFile.name} file is displayed on the File List Page.")
+ Log.d(STEP_TAG,"Select 'unfiled' directory. Assert that '${discussionAttachmentFile.name}' file is displayed on the File List Page.")
fileListPage.selectItem("unfiled")
fileListPage.assertItemDisplayed(discussionAttachmentFile.name)
val newFileName = "newTextFileName.txt"
- Log.d(STEP_TAG,"Rename ${discussionAttachmentFile.name} file to: $newFileName.")
+ Log.d(STEP_TAG,"Rename '${discussionAttachmentFile.name}' file to: '$newFileName'.")
fileListPage.renameFile(discussionAttachmentFile.name, newFileName)
- Log.d(STEP_TAG,"Assert that the file is displayed with it's new file name: $newFileName.")
+ Log.d(STEP_TAG,"Assert that the file is displayed with it's new file name: '$newFileName'.")
fileListPage.assertItemDisplayed(newFileName)
- Log.d(STEP_TAG,"Delete $newFileName file.")
+ Log.d(STEP_TAG,"Delete '$newFileName' file.")
fileListPage.deleteFile(newFileName)
Log.d(STEP_TAG,"Assert that empty view is displayed after deletion.")
@@ -224,54 +224,4 @@ class FilesE2ETest: StudentTest() {
Log.d(STEP_TAG,"Assert that there is a folder called '$testFolderName' is displayed.")
fileListPage.assertItemDisplayed(testFolderName)
}
-
- private fun commentOnSubmission(
- student: CanvasUserApiModel,
- course: CourseApiModel,
- assignment: AssignmentApiModel,
- commentUploadInfo: AttachmentApiModel
- ) {
- SubmissionsApi.commentOnSubmission(
- studentToken = student.token,
- courseId = course.id,
- assignmentId = assignment.id,
- fileIds = mutableListOf(commentUploadInfo.id)
- )
- }
-
- private fun createAssignment(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) = AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = course.id,
- withDescription = false,
- submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD),
- allowedExtensions = listOf("txt"),
- teacherToken = teacher.token
- )
- )
-
- private fun submitAssignment(
- course: CourseApiModel,
- assignment: AssignmentApiModel,
- submissionUploadInfo: AttachmentApiModel,
- student: CanvasUserApiModel
- ) {
- SubmissionsApi.submitCourseAssignment(
- submissionType = SubmissionType.ONLINE_UPLOAD,
- courseId = course.id,
- assignmentId = assignment.id,
- fileIds = mutableListOf(submissionUploadInfo.id),
- studentToken = student.token
- )
- }
-
- private fun createDiscussion(
- course: CourseApiModel,
- student: CanvasUserApiModel
- ) = DiscussionTopicsApi.createDiscussion(
- courseId = course.id,
- token = student.token
- )
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt
index c176601e99..253bc5dcdf 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt
@@ -12,9 +12,6 @@ import com.instructure.canvas.espresso.containsTextCaseInsensitive
import com.instructure.dataseeding.api.AssignmentsApi
import com.instructure.dataseeding.api.QuizzesApi
import com.instructure.dataseeding.api.SubmissionsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.GradingType
import com.instructure.dataseeding.model.QuizAnswer
import com.instructure.dataseeding.model.QuizQuestion
@@ -31,6 +28,7 @@ import org.junit.Test
@HiltAndroidTest
class GradesE2ETest: StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -46,9 +44,9 @@ class GradesE2ETest: StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.")
- val assignment = createAssignment(course, teacher)
- val assignment2 = createAssignment(course, teacher)
+ Log.d(PREPARATION_TAG,"Seeding assignment for '${course.name}' course.")
+ val assignment = AssignmentsApi.createAssignment(course.id, teacher.token, withDescription = true, gradingType = GradingType.PERCENT, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601)
+ val assignment2 = AssignmentsApi.createAssignment(course.id, teacher.token, withDescription = true, gradingType = GradingType.PERCENT, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601)
Log.d(PREPARATION_TAG,"Create a quiz with some questions.")
val quizQuestions = makeQuizQuestions()
@@ -60,7 +58,7 @@ class GradesE2ETest: StudentTest() {
tokenLogin(student)
dashboardPage.waitForRender()
- Log.d(STEP_TAG,"Select ${course.name} course.")
+ Log.d(STEP_TAG,"Select '${course.name}' course.")
dashboardPage.selectCourse(course)
Log.d(STEP_TAG,"Navigate to Grades Page.")
@@ -71,7 +69,7 @@ class GradesE2ETest: StudentTest() {
val assignmentMatcher = withText(assignment.name)
val quizMatcher = withText(quiz.title)
- Log.d(STEP_TAG,"Refresh the page. Assert that the ${assignment.name} assignment and ${quiz.title} quiz are displayed and there is no grade for them.")
+ Log.d(STEP_TAG,"Refresh the page. Assert that the '${assignment.name}' assignment and '${quiz.title}' quiz are displayed and there is no grade for them.")
courseGradesPage.refresh()
courseGradesPage.assertItemDisplayed(assignmentMatcher)
courseGradesPage.assertGradeNotDisplayed(assignmentMatcher)
@@ -81,7 +79,7 @@ class GradesE2ETest: StudentTest() {
Log.d(STEP_TAG,"Check in the 'What-If Score' checkbox.")
courseGradesPage.toggleWhatIf()
- Log.d(STEP_TAG,"Enter '12' as a what-if grade for ${assignment.name} assignment.")
+ Log.d(STEP_TAG,"Enter '12' as a what-if grade for '${assignment.name}' assignment.")
courseGradesPage.enterWhatIfGrade(assignmentMatcher, "12")
Log.d(STEP_TAG,"Assert that 'Total Grade' contains the score '80%'.")
@@ -94,10 +92,10 @@ class GradesE2ETest: StudentTest() {
courseGradesPage.assertTotalGrade(withText(R.string.noGradeText))
Log.d(PREPARATION_TAG,"Seed a submission for '${assignment.name}' assignment.")
- submitAssignment(course, assignment, student)
+ SubmissionsApi.submitCourseAssignment(course.id, student.token, assignment.id, SubmissionType.ONLINE_TEXT_ENTRY)
Log.d(PREPARATION_TAG,"Grade the previously seeded submission for '${assignment.name}' assignment.")
- gradeSubmission(teacher, course, assignment, student, "9",false)
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, assignment.id, student.id, postedGrade = "9")
Log.d(STEP_TAG,"Refresh the page. Assert that the assignment's score is '60%'.")
courseGradesPage.refresh()
@@ -114,10 +112,10 @@ class GradesE2ETest: StudentTest() {
courseGradesPage.refreshUntilAssertTotalGrade(containsTextCaseInsensitive("60"))
Log.d(PREPARATION_TAG,"Seed a submission for '${assignment2.name}' assignment.")
- submitAssignment(course, assignment2, student)
+ SubmissionsApi.submitCourseAssignment(course.id, student.token, assignment2.id, SubmissionType.ONLINE_TEXT_ENTRY)
Log.d(PREPARATION_TAG,"Grade the previously seeded submission for '${assignment2.name}' assignment.")
- gradeSubmission(teacher, course, assignment2, student, "10", excused = false)
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, assignment2.id, student.id, postedGrade = "10")
Log.d(STEP_TAG,"Assert that we can see the correct score at the '${assignment2.name}' assignment (66.67%) and at the total score as well (63.33%).")
courseGradesPage.refresh()
@@ -128,13 +126,13 @@ class GradesE2ETest: StudentTest() {
courseGradesPage.refreshUntilAssertTotalGrade(containsTextCaseInsensitive("63.33"))
Log.d(PREPARATION_TAG,"Grade the previously seeded submission for '${assignment.name}' assignment.")
- gradeSubmission(teacher, course, assignment, student, excused = true)
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, assignment.id, student.id, excused = true)
courseGradesPage.refresh()
Log.d(STEP_TAG,"Assert that we can see the correct score (66.67%).")
courseGradesPage.refreshUntilAssertTotalGrade(containsTextCaseInsensitive("66.67"))
- gradeSubmission(teacher, course, assignment, student, "9",false)
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, assignment.id, student.id, postedGrade = "9")
courseGradesPage.refresh()
Log.d(STEP_TAG,"Assert that we can see the correct score (63.33%).")
@@ -194,54 +192,4 @@ class GradesE2ETest: StudentTest() {
)
)
-
- private fun createAssignment(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = course.id,
- withDescription = true,
- dueAt = 1.days.fromNow.iso8601,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- teacherToken = teacher.token,
- gradingType = GradingType.PERCENT,
- pointsPossible = 15.0
- )
- )
- }
-
- private fun submitAssignment(
- course: CourseApiModel,
- assignment: AssignmentApiModel,
- student: CanvasUserApiModel
- ) {
- SubmissionsApi.submitCourseAssignment(
- submissionType = SubmissionType.ONLINE_TEXT_ENTRY,
- courseId = course.id,
- assignmentId = assignment.id,
- fileIds = mutableListOf(),
- studentToken = student.token
- )
- }
-
- private fun gradeSubmission(
- teacher: CanvasUserApiModel,
- course: CourseApiModel,
- assignment: AssignmentApiModel,
- student: CanvasUserApiModel,
- postedGrade: String? = null,
- excused: Boolean,
- ) {
- SubmissionsApi.gradeSubmission(
- teacherToken = teacher.token,
- courseId = course.id,
- assignmentId = assignment.id,
- studentId = student.id,
- postedGrade = postedGrade,
- excused = excused
- )
- }
-
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt
index 2008a63052..7ee14b2b5e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt
@@ -28,8 +28,6 @@ import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvas.espresso.refresh
import com.instructure.dataseeding.api.ConversationsApi
import com.instructure.dataseeding.api.GroupsApi
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.espresso.retry
import com.instructure.espresso.retryWithIncreasingDelay
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
@@ -47,6 +45,7 @@ class InboxE2ETest: StudentTest() {
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.INBOX, TestCategory.E2E)
fun testInboxSelectedButtonActionsE2E() {
+
Log.d(PREPARATION_TAG, "Seeding data.")
val data = seedData(students = 2, teachers = 1, courses = 1)
val teacher = data.teachersList[0]
@@ -54,9 +53,11 @@ class InboxE2ETest: StudentTest() {
val student1 = data.studentsList[0]
val student2 = data.studentsList[1]
+ Log.d(PREPARATION_TAG, "Create a course group category and a group based on that category.")
val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token)
val group = GroupsApi.createGroup(groupCategory.id, teacher.token)
- Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} and ${student2.name} students to the group: ${group.name}.")
+
+ Log.d(PREPARATION_TAG, "Create group membership for '${student1.name}' and '${student2.name}' students to the group: '${group.name}'.")
GroupsApi.createGroupMembership(group.id, student1.id, teacher.token)
GroupsApi.createGroupMembership(group.id, student2.id, teacher.token)
@@ -69,19 +70,19 @@ class InboxE2ETest: StudentTest() {
dashboardPage.clickInboxTab()
inboxPage.assertInboxEmpty()
- Log.d(PREPARATION_TAG,"Seed an email from the teacher to ${student1.name} and ${student2.name} students.")
- val seededConversation = createConversation(teacher, student1, student2)[0]
+ Log.d(PREPARATION_TAG,"Seed an email from the teacher to '${student1.name}' and '${student2.name}' students.")
+ val seededConversation = ConversationsApi.createConversation(teacher.token, listOf(student1.id.toString(), student2.id.toString()))[0]
Log.d(STEP_TAG,"Refresh the page. Assert that there is a conversation and it is the previously seeded one.")
refresh()
inboxPage.assertHasConversation()
inboxPage.assertConversationDisplayed(seededConversation)
- Log.d(STEP_TAG,"Select ${seededConversation.subject} conversation. Assert that is has not been starred already.")
+ Log.d(STEP_TAG,"Select '${seededConversation.subject}' conversation. Assert that is has not been starred already.")
inboxPage.openConversation(seededConversation)
inboxConversationPage.assertNotStarred()
- Log.d(STEP_TAG,"Toggle Starred to mark ${seededConversation.subject} conversation as favourite. Assert that it has became starred.")
+ Log.d(STEP_TAG,"Toggle Starred to mark '${seededConversation.subject}' conversation as favourite. Assert that it has became starred.")
inboxConversationPage.toggleStarred()
inboxConversationPage.assertStarred()
@@ -89,25 +90,25 @@ class InboxE2ETest: StudentTest() {
Espresso.pressBack() // To main inbox page
inboxPage.assertConversationStarred(seededConversation.subject)
- Log.d(STEP_TAG,"Select ${seededConversation.subject} conversation. Mark as Unread by clicking on the 'More Options' menu, 'Mark as Unread' menu point.")
+ Log.d(STEP_TAG,"Select '${seededConversation.subject}' conversation. Mark as Unread by clicking on the 'More Options' menu, 'Mark as Unread' menu point.")
inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.GONE)
inboxPage.openConversation(seededConversation)
inboxConversationPage.markUnread() //After select 'Mark as Unread', we will be navigated back to Inbox Page
- Log.d(STEP_TAG,"Assert that ${seededConversation.subject} conversation has been marked as unread.")
+ Log.d(STEP_TAG,"Assert that '${seededConversation.subject}' conversation has been marked as unread.")
inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.VISIBLE)
- Log.d(STEP_TAG,"Select ${seededConversation.subject} conversation. Archive it by clicking on the 'More Options' menu, 'Archive' menu point.")
+ Log.d(STEP_TAG,"Select '${seededConversation.subject}' conversation. Archive it by clicking on the 'More Options' menu, 'Archive' menu point.")
inboxPage.openConversation(seededConversation)
inboxConversationPage.archive() //After select 'Archive', we will be navigated back to Inbox Page
- Log.d(STEP_TAG,"Assert that ${seededConversation.subject} conversation has removed from 'All' tab.") //TODO: Discuss this logic if it's ok if we don't show Archived messages on 'All' tab...
+ Log.d(STEP_TAG,"Assert that '${seededConversation.subject}' conversation has removed from 'All' tab.") //TODO: Discuss this logic if it's ok if we don't show Archived messages on 'All' tab...
inboxPage.assertConversationNotDisplayed(seededConversation)
Log.d(STEP_TAG,"Select 'Archived' conversation filter.")
inboxPage.filterInbox("Archived")
- Log.d(STEP_TAG,"Assert that ${seededConversation.subject} conversation is displayed by the 'Archived' filter.")
+ Log.d(STEP_TAG,"Assert that '${seededConversation.subject}' conversation is displayed by the 'Archived' filter.")
inboxPage.assertConversationDisplayed(seededConversation)
Log.d(STEP_TAG, "Select '${seededConversation.subject}' conversation. Assert that the selected number of conversations on the toolbar is 1." +
@@ -115,18 +116,17 @@ class InboxE2ETest: StudentTest() {
inboxPage.selectConversation(seededConversation)
inboxPage.assertSelectedConversationNumber("1")
inboxPage.clickUnArchive()
-
inboxPage.assertInboxEmpty()
inboxPage.assertConversationNotDisplayed(seededConversation.subject)
sleep(2000)
- Log.d(STEP_TAG,"Navigate to 'INBOX' scope and assert that ${seededConversation.subject} conversation is displayed.")
+ Log.d(STEP_TAG,"Navigate to 'INBOX' scope and assert that '${seededConversation.subject}' conversation is displayed.")
inboxPage.filterInbox("Inbox")
inboxPage.assertConversationDisplayed(seededConversation.subject)
- Log.d(STEP_TAG, "Select the conversations (${seededConversation.subject} and star it." +
- "Assert that the selected number of conversations on the toolbar is 1 and the conversation is starred.")
+ Log.d(STEP_TAG, "Select the conversation '${seededConversation.subject}' and unstar it." +
+ "Assert that the selected number of conversations on the toolbar is 1 and the conversation is not starred.")
inboxPage.selectConversations(listOf(seededConversation.subject))
inboxPage.assertSelectedConversationNumber("1")
inboxPage.clickUnstar()
@@ -135,16 +135,16 @@ class InboxE2ETest: StudentTest() {
inboxPage.assertConversationNotStarred(seededConversation.subject)
}
- Log.d(STEP_TAG, "Select the conversations (${seededConversation.subject} and archive it. Assert that it has not displayed in the 'INBOX' scope.")
+ Log.d(STEP_TAG, "Select the conversation '${seededConversation.subject}' and archive it. Assert that it has not displayed in the 'INBOX' scope.")
inboxPage.selectConversations(listOf(seededConversation.subject))
inboxPage.clickArchive()
inboxPage.assertConversationNotDisplayed(seededConversation.subject)
- sleep(2000)
-
Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope and assert that the conversation is displayed there.")
inboxPage.filterInbox("Archived")
- inboxPage.assertConversationDisplayed(seededConversation.subject)
+ retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) {
+ inboxPage.assertConversationDisplayed(seededConversation.subject)
+ }
Log.d(STEP_TAG, "Navigate to 'UNREAD' scope and assert that the conversation is displayed there, because a conversation cannot be archived and unread at the same time.")
inboxPage.filterInbox("Unread")
@@ -154,7 +154,7 @@ class InboxE2ETest: StudentTest() {
inboxPage.filterInbox("Starred")
inboxPage.assertConversationNotDisplayed(seededConversation.subject)
- Log.d(STEP_TAG,"Navigate to 'INBOX' scope and assert that ${seededConversation.subject} conversation is NOT displayed because it is archived yet.")
+ Log.d(STEP_TAG,"Navigate to 'INBOX' scope and assert that '${seededConversation.subject}' conversation is NOT displayed because it is archived yet.")
inboxPage.filterInbox("Inbox")
inboxPage.assertConversationNotDisplayed(seededConversation.subject)
@@ -176,8 +176,6 @@ class InboxE2ETest: StudentTest() {
inboxPage.filterInbox("Starred")
inboxPage.assertConversationDisplayed(seededConversation.subject)
- sleep(2000)
-
Log.d(STEP_TAG, "Navigate to 'INBOX' scope and assert that the conversation is displayed there because it is not archived yet.")
inboxPage.filterInbox("Inbox")
inboxPage.assertConversationDisplayed(seededConversation.subject)
@@ -195,9 +193,11 @@ class InboxE2ETest: StudentTest() {
val student1 = data.studentsList[0]
val student2 = data.studentsList[1]
+ Log.d(PREPARATION_TAG, "Create a course group category and a group based on that category.")
val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token)
val group = GroupsApi.createGroup(groupCategory.id, teacher.token)
- Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} and ${student2.name} students to the group: ${group.name}.")
+
+ Log.d(PREPARATION_TAG, "Create group membership for '${student1.name}' and '${student2.name}' students to the group: '${group.name}'.")
GroupsApi.createGroupMembership(group.id, student1.id, teacher.token)
GroupsApi.createGroupMembership(group.id, student2.id, teacher.token)
@@ -210,8 +210,8 @@ class InboxE2ETest: StudentTest() {
dashboardPage.clickInboxTab()
inboxPage.assertInboxEmpty()
- Log.d(PREPARATION_TAG,"Seed an email from the teacher to ${student1.name} and ${student2.name} students.")
- val seededConversation = createConversation(teacher, student1, student2)[0]
+ Log.d(PREPARATION_TAG,"Seed an email from the teacher to '${student1.name}' and '${student2.name}' students.")
+ val seededConversation = ConversationsApi.createConversation(teacher.token, listOf(student1.id.toString(), student2.id.toString()))[0]
Log.d(STEP_TAG,"Refresh the page. Assert that there is a conversation and it is the previously seeded one.")
refresh()
@@ -223,7 +223,7 @@ class InboxE2ETest: StudentTest() {
val newMessageSubject = "Hey There"
val newMessage = "Just checking in"
- Log.d(STEP_TAG,"Create a new message with subject: $newMessageSubject, and message: $newMessage")
+ Log.d(STEP_TAG,"Create a new message with subject: '$newMessageSubject', and message: '$newMessage'")
newMessagePage.populateMessage(course, student2, newMessageSubject, newMessage)
Log.d(STEP_TAG,"Click on 'Send' button.")
@@ -234,7 +234,7 @@ class InboxE2ETest: StudentTest() {
val newGroupMessageSubject = "Group Message"
val newGroupMessage = "Testing Group ${group.name}"
- Log.d(STEP_TAG,"Create a new message with subject: $newGroupMessageSubject, and message: $newGroupMessage")
+ Log.d(STEP_TAG,"Create a new message with subject: '$newGroupMessageSubject', and message: '$newGroupMessage'")
newMessagePage.populateGroupMessage(group, student2, newGroupMessageSubject, newGroupMessage)
Log.d(STEP_TAG,"Click on 'Send' button.")
@@ -246,7 +246,7 @@ class InboxE2ETest: StudentTest() {
inboxPage.goToDashboard()
dashboardPage.waitForRender()
- Log.d(STEP_TAG,"Log out with ${student1.name} student.")
+ Log.d(STEP_TAG,"Log out with '${student1.name}' student.")
leftSideNavigationDrawerPage.logout()
Log.d(STEP_TAG,"Login with user: ${student2.name}, login id: ${student2.loginId}.")
@@ -259,13 +259,13 @@ class InboxE2ETest: StudentTest() {
inboxPage.assertConversationDisplayed(newMessageSubject)
inboxPage.assertConversationDisplayed("Group Message")
- Log.d(STEP_TAG,"Select $newGroupMessageSubject conversation.")
+ Log.d(STEP_TAG,"Select '$newGroupMessageSubject' conversation.")
inboxPage.openConversation(newMessageSubject)
val newReplyMessage = "This is a quite new reply message."
Log.d(STEP_TAG,"Reply to $newGroupMessageSubject conversation with '$newReplyMessage' message. Assert that the reply is displayed.")
inboxConversationPage.replyToMessage(newReplyMessage)
- Log.d(STEP_TAG,"Delete $newReplyMessage reply and assert is has been deleted.")
+ Log.d(STEP_TAG,"Delete '$newReplyMessage' reply and assert is has been deleted.")
inboxConversationPage.deleteMessage(newReplyMessage)
inboxConversationPage.assertMessageNotDisplayed(newReplyMessage)
@@ -275,7 +275,7 @@ class InboxE2ETest: StudentTest() {
inboxPage.assertConversationDisplayed(seededConversation)
inboxPage.assertConversationDisplayed("Group Message")
- Log.d(STEP_TAG, "Navigate to 'INBOX' scope and seledct '$newGroupMessageSubject' conversation.")
+ Log.d(STEP_TAG, "Navigate to 'INBOX' scope and select '$newGroupMessageSubject' conversation.")
inboxPage.filterInbox("Inbox")
inboxPage.selectConversation(newGroupMessageSubject)
@@ -289,6 +289,7 @@ class InboxE2ETest: StudentTest() {
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.INBOX, TestCategory.E2E)
fun testInboxSwipeGesturesE2E() {
+
Log.d(PREPARATION_TAG, "Seeding data.")
val data = seedData(students = 2, teachers = 1, courses = 1)
val teacher = data.teachersList[0]
@@ -296,9 +297,11 @@ class InboxE2ETest: StudentTest() {
val student1 = data.studentsList[0]
val student2 = data.studentsList[1]
+ Log.d(PREPARATION_TAG, "Create a course group category and a group based on that category.")
val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token)
val group = GroupsApi.createGroup(groupCategory.id, teacher.token)
- Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} and ${student2.name} students to the group: ${group.name}.")
+
+ Log.d(PREPARATION_TAG, "Create group membership for '${student1.name}' and '${student2.name}' students to the group: '${group.name}'.")
GroupsApi.createGroupMembership(group.id, student1.id, teacher.token)
GroupsApi.createGroupMembership(group.id, student2.id, teacher.token)
@@ -311,8 +314,8 @@ class InboxE2ETest: StudentTest() {
dashboardPage.clickInboxTab()
inboxPage.assertInboxEmpty()
- Log.d(PREPARATION_TAG,"Seed an email from the teacher to ${student1.name} and ${student2.name} students.")
- val seededConversation = createConversation(teacher, student1, student2)[0]
+ Log.d(PREPARATION_TAG,"Seed an email from the teacher to '${student1.name}' and '${student2.name}' students.")
+ val seededConversation = ConversationsApi.createConversation(teacher.token, listOf(student1.id.toString(), student2.id.toString()))[0]
Log.d(STEP_TAG,"Refresh the page. Assert that there is a conversation and it is the previously seeded one.")
refresh()
@@ -351,12 +354,10 @@ class InboxE2ETest: StudentTest() {
inboxPage.assertConversationStarred(seededConversation.subject)
inboxPage.clickMarkAsUnread()
- sleep(1000)
-
Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that the conversation is displayed in the 'STARRED' scope.")
inboxPage.filterInbox("Starred")
- retry(times = 10, delay = 3000) {
+ retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) {
inboxPage.assertConversationDisplayed(seededConversation.subject)
}
@@ -410,7 +411,7 @@ class InboxE2ETest: StudentTest() {
inboxPage.openConversationWithRecipients(recipientList)
inboxConversationPage.assertMessageDisplayed(questionText)
- Log.d(STEP_TAG,"Log out with ${student.name} student.")
+ Log.d(STEP_TAG,"Log out with '${student.name}' student.")
Espresso.pressBack()
leftSideNavigationDrawerPage.logout()
@@ -418,9 +419,10 @@ class InboxE2ETest: StudentTest() {
tokenLogin(teacher)
dashboardPage.waitForRender()
- Log.d(STEP_TAG,"Open Inbox Page. Assert that the asked question is displayed in the teacher's inbox with the proper recipients ($recipientList) and message ($questionText).")
+ Log.d(STEP_TAG,"Open Inbox Page. Assert that the asked question is displayed in the teacher's inbox with the proper recipients ($recipientList), subject and message ($questionText).")
dashboardPage.clickInboxTab()
inboxPage.assertConversationWithRecipientsDisplayed(recipientList)
+ inboxPage.assertConversationSubject("(No Subject)")
inboxPage.assertConversationDisplayed(questionText)
Log.d(STEP_TAG, "Open the conversation and assert that there is no subject of the conversation and the message body is equal to which the student typed in the 'Ask Your Instructor' dialog: '$questionText'.")
@@ -428,13 +430,4 @@ class InboxE2ETest: StudentTest() {
inboxConversationPage.assertMessageDisplayed(questionText)
inboxConversationPage.assertNoSubjectDisplayed()
}
-
- private fun createConversation(
- teacher: CanvasUserApiModel,
- student1: CanvasUserApiModel,
- student2: CanvasUserApiModel
- ) = ConversationsApi.createConversation(
- token = teacher.token,
- recipients = listOf(student1.id.toString(), student2.id.toString())
- )
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt
index c9d3d7b60a..12526c1bac 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt
@@ -39,6 +39,7 @@ import org.junit.Test
@HiltAndroidTest
class LoginE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -59,7 +60,7 @@ class LoginE2ETest : StudentTest() {
Log.d(STEP_TAG, "Assert that the Dashboard Page is the landing page and it is loaded successfully.")
assertDashboardPageDisplayed(student1)
- Log.d(STEP_TAG, "Log out with ${student1.name} student.")
+ Log.d(STEP_TAG, "Log out with '${student1.name}' student.")
leftSideNavigationDrawerPage.logout()
Log.d(STEP_TAG, "Login with user: ${student2.name}, login id: ${student2.loginId}.")
@@ -83,15 +84,15 @@ class LoginE2ETest : StudentTest() {
Log.d(STEP_TAG, "Click on 'Change User' button on the left-side menu.")
leftSideNavigationDrawerPage.clickChangeUserMenu()
- Log.d(STEP_TAG, "Assert that the previously logins has been displayed. Assert that ${student1.name} and ${student2.name} students are displayed within the previous login section.")
+ Log.d(STEP_TAG, "Assert that the previously logins has been displayed. Assert that '${student1.name}' and '${student2.name}' students are displayed within the previous login section.")
loginLandingPage.assertDisplaysPreviousLogins()
loginLandingPage.assertPreviousLoginUserDisplayed(student1.name)
loginLandingPage.assertPreviousLoginUserDisplayed(student2.name)
- Log.d(STEP_TAG, "Remove ${student1.name} student from the previous login section.")
+ Log.d(STEP_TAG, "Remove '${student1.name}' student from the previous login section.")
loginLandingPage.removeUserFromPreviousLogins(student1.name)
- Log.d(STEP_TAG, "Login with the previous user, ${student2.name}, with one click, by clicking on the user's name on the bottom.")
+ Log.d(STEP_TAG, "Login with the previous user, '${student2.name}', with one click, by clicking on the user's name on the bottom.")
loginLandingPage.loginWithPreviousUser(student2)
Log.d(STEP_TAG, "Assert that the Dashboard Page is the landing page and it is loaded successfully.")
@@ -100,18 +101,17 @@ class LoginE2ETest : StudentTest() {
Log.d(STEP_TAG, "Click on 'Change User' button on the left-side menu.")
leftSideNavigationDrawerPage.clickChangeUserMenu()
- Log.d(STEP_TAG, "Assert that the previously logins has been displayed. Assert that ${student1.name} and ${student2.name} students are displayed within the previous login section.")
+ Log.d(STEP_TAG, "Assert that the previously logins has been displayed. Assert that '${student1.name}' and '${student2.name}' students are displayed within the previous login section.")
loginLandingPage.assertDisplaysPreviousLogins()
loginLandingPage.assertPreviousLoginUserDisplayed(student2.name)
- Log.d(STEP_TAG, "Remove ${student2.name} student from the previous login section.")
+ Log.d(STEP_TAG, "Remove '${student2.name}' student from the previous login section.")
loginLandingPage.removeUserFromPreviousLogins(student2.name)
- Log.d(STEP_TAG, "Assert that none of the students, ${student1.name} and ${student2.name} are displayed and not even the 'Previous Logins' label is displayed.")
+ Log.d(STEP_TAG, "Assert that none of the students, '${student1.name}' and '${student2.name}' are displayed and not even the 'Previous Logins' label is displayed.")
loginLandingPage.assertPreviousLoginUserNotExist(student1.name)
loginLandingPage.assertPreviousLoginUserNotExist(student2.name)
loginLandingPage.assertNotDisplaysPreviousLogins()
-
}
@E2E
@@ -138,7 +138,6 @@ class LoginE2ETest : StudentTest() {
Log.d(STEP_TAG, "Assert that the Dashboard Page is the landing page and it is loaded successfully.")
assertDashboardPageDisplayed(student2)
-
}
@E2E
@@ -162,37 +161,37 @@ class LoginE2ETest : StudentTest() {
Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
loginWithUser(student)
- Log.d(STEP_TAG,"Validate ${student.name} user's role as a Student.")
+ Log.d(STEP_TAG,"Validate '${student.name}' user's role as a Student.")
validateUserAndRole(student, course, "Student")
Log.d(STEP_TAG,"Navigate back to Dashboard Page.")
ViewUtils.pressBackButton(2)
- Log.d(STEP_TAG,"Log out with ${student.name} student.")
+ Log.d(STEP_TAG,"Log out with '${student.name}' student.")
leftSideNavigationDrawerPage.logout()
Log.d(STEP_TAG,"Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
loginWithUser(teacher, true)
- Log.d(STEP_TAG,"Validate ${teacher.name} user's role as a Teacher.")
+ Log.d(STEP_TAG,"Validate '${teacher.name}' user's role as a Teacher.")
validateUserAndRole(teacher, course, "Teacher")
Log.d(STEP_TAG,"Navigate back to Dashboard Page.")
ViewUtils.pressBackButton(2)
- Log.d(STEP_TAG,"Log out with ${teacher.name} teacher.")
+ Log.d(STEP_TAG,"Log out with '${teacher.name}' teacher.")
leftSideNavigationDrawerPage.logout()
Log.d(STEP_TAG,"Login with user: ${ta.name}, login id: ${ta.loginId}.")
loginWithUser(ta, true)
- Log.d(STEP_TAG,"Validate ${ta.name} user's role as a TA.")
+ Log.d(STEP_TAG,"Validate '${ta.name}' user's role as a TA (Teacher Assistant).")
validateUserAndRole(ta, course, "TA")
Log.d(STEP_TAG,"Navigate back to Dashboard Page.")
ViewUtils.pressBackButton(2)
- Log.d(STEP_TAG,"Log out with ${ta.name} teacher assistant.")
+ Log.d(STEP_TAG,"Log out with '${ta.name}' teacher assistant.")
leftSideNavigationDrawerPage.logout()
Log.d(STEP_TAG,"Login with user: ${parent.name}, login id: ${parent.loginId}.")
@@ -201,7 +200,7 @@ class LoginE2ETest : StudentTest() {
Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
assertDashboardPageDisplayed(parent)
- Log.d(STEP_TAG,"Log out with ${parent.name} parent.")
+ Log.d(STEP_TAG,"Log out with '${parent.name}' parent.")
leftSideNavigationDrawerPage.logout()
}
@@ -261,43 +260,29 @@ class LoginE2ETest : StudentTest() {
val enrollmentsService = retrofitClient.create(EnrollmentsApi.EnrollmentsService::class.java)
Log.d(PREPARATION_TAG,"Create student, teacher, and a course via API.")
- val student = UserApi.createCanvasUser(userService = userService, userDomain = domain)
- val teacher = UserApi.createCanvasUser(userService = userService, userDomain = domain)
+ val student = UserApi.createCanvasUser(userService, domain)
+ val teacher = UserApi.createCanvasUser(userService, domain)
val course = CoursesApi.createCourse(coursesService = coursesService)
- Log.d(PREPARATION_TAG,"Enroll ${student.name} student to ${course.name} course.")
- enrollUser(course, student, STUDENT_ENROLLMENT, enrollmentsService)
+ Log.d(PREPARATION_TAG,"Enroll '${student.name}' student to '${course.name}' course.")
+ EnrollmentsApi.enrollUser(course.id, student.id, STUDENT_ENROLLMENT, enrollmentsService)
- Log.d(PREPARATION_TAG,"Enroll ${teacher.name} teacher to ${course.name} course.")
- enrollUser(course, teacher, TEACHER_ENROLLMENT, enrollmentsService)
+ Log.d(PREPARATION_TAG,"Enroll '${teacher.name}' teacher to '${course.name}' course.")
+ EnrollmentsApi.enrollUser(course.id, teacher.id, TEACHER_ENROLLMENT, enrollmentsService)
Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
loginWithUser(student)
- Log.d(STEP_TAG,"Attempt to sign into our vanity domain, and validate ${student.name} user's role as a Student.")
+ Log.d(STEP_TAG,"Attempt to sign into our vanity domain, and validate '${student.name}' user's role as a Student.")
validateUserAndRole(student, course,"Student" )
Log.d(STEP_TAG,"Navigate back to Dashboard Page.")
ViewUtils.pressBackButton(2)
- Log.d(STEP_TAG,"Log out with ${student.name} student.")
+ Log.d(STEP_TAG,"Log out with '${student.name}' student.")
leftSideNavigationDrawerPage.logout()
}
- private fun enrollUser(
- course: CourseApiModel,
- student: CanvasUserApiModel,
- enrollmentType: String,
- enrollmentsService: EnrollmentsApi.EnrollmentsService
- ) {
- EnrollmentsApi.enrollUser(
- courseId = course.id,
- userId = student.id,
- enrollmentType = enrollmentType,
- enrollmentService = enrollmentsService
- )
- }
-
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.
@@ -311,7 +296,7 @@ class LoginE2ETest : StudentTest() {
loginLandingPage.clickFindMySchoolButton()
}
- Log.d(STEP_TAG,"Enter domain: ${user.domain}.")
+ Log.d(STEP_TAG,"Enter domain: '${user.domain}'.")
loginFindSchoolPage.enterDomain(user.domain)
Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.")
@@ -324,7 +309,7 @@ class LoginE2ETest : StudentTest() {
Log.d(STEP_TAG, "Click on last saved school's button.")
loginLandingPage.clickOnLastSavedSchoolButton()
- Log.d(STEP_TAG, "Login with ${user.name} user.")
+ Log.d(STEP_TAG, "Login with '${user.name}' user.")
loginSignInPage.loginAs(user)
}
@@ -333,11 +318,11 @@ class LoginE2ETest : StudentTest() {
Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
assertDashboardPageDisplayed(user)
- Log.d(STEP_TAG,"Navigate to 'People' Page of ${course.name} course.")
+ Log.d(STEP_TAG,"Navigate to 'People' Page of '${course.name}' course.")
dashboardPage.selectCourse(course)
courseBrowserPage.selectPeople()
- Log.d(STEP_TAG,"Assert that ${user.name} user's role is: $role.")
+ Log.d(STEP_TAG,"Assert that '${user.name}' user's role is: $role.")
peopleListPage.assertPersonListed(user, role)
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt
index 862b6a0c87..cf15715843 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt
@@ -28,9 +28,7 @@ import com.instructure.dataseeding.api.DiscussionTopicsApi
import com.instructure.dataseeding.api.ModulesApi
import com.instructure.dataseeding.api.PagesApi
import com.instructure.dataseeding.api.QuizzesApi
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
-import com.instructure.dataseeding.model.ModuleApiModel
+import com.instructure.dataseeding.model.GradingType
import com.instructure.dataseeding.model.ModuleItemTypes
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.days
@@ -44,6 +42,7 @@ import org.junit.Test
@HiltAndroidTest
class ModulesE2ETest: StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -59,51 +58,51 @@ class ModulesE2ETest: StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.")
- val assignment1 = createAssignment(course, true, teacher, 1.days.fromNow.iso8601)
+ Log.d(PREPARATION_TAG,"Seeding assignment for '${course.name}' course.")
+ val assignment1 = AssignmentsApi.createAssignment(course.id, teacher.token, withDescription = true, gradingType = GradingType.POINTS, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
- Log.d(PREPARATION_TAG,"Seeding another assignment for ${course.name} course.")
- val assignment2 = createAssignment(course, true, teacher, 2.days.fromNow.iso8601)
+ Log.d(PREPARATION_TAG,"Seeding another assignment for '${course.name}' course.")
+ val assignment2 = AssignmentsApi.createAssignment(course.id, teacher.token, dueAt = 2.days.fromNow.iso8601, withDescription = true, gradingType = GradingType.POINTS, pointsPossible = 15.0, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
- Log.d(PREPARATION_TAG,"Create a PUBLISHED quiz for ${course.name} course.")
- val quiz1 = createQuiz(course, teacher)
+ Log.d(PREPARATION_TAG,"Create a PUBLISHED quiz for '${course.name}' course.")
+ val quiz1 = QuizzesApi.createQuiz(course.id, teacher.token, dueAt = 3.days.fromNow.iso8601)
- Log.d(PREPARATION_TAG,"Create a page for ${course.name} course.")
- val page1 = createCoursePage(course, teacher)
+ Log.d(PREPARATION_TAG,"Create a page for '${course.name}' course.")
+ val page1 = PagesApi.createCoursePage(course.id, teacher.token)
- Log.d(PREPARATION_TAG,"Create a discussion topic for ${course.name} course.")
- val discussionTopic1 = createDiscussion(course, teacher)
+ Log.d(PREPARATION_TAG,"Create a discussion topic for '${course.name}' course.")
+ val discussionTopic1 = DiscussionTopicsApi.createDiscussion(course.id, teacher.token)
//Modules start out as unpublished.
- Log.d(PREPARATION_TAG,"Create a module for ${course.name} course.")
- val module1 = createModule(course, teacher)
+ Log.d(PREPARATION_TAG,"Create a module for '${course.name}' course.")
+ val module1 = ModulesApi.createModule(course.id, teacher.token)
- Log.d(PREPARATION_TAG,"Create another module for ${course.name} course.")
- val module2 = createModule(course, teacher)
+ Log.d(PREPARATION_TAG,"Create another module for '${course.name}' course.")
+ val module2 = ModulesApi.createModule(course.id, teacher.token)
- Log.d(PREPARATION_TAG,"Associate ${assignment1.name} assignment with ${module1.name} module.")
- createModuleItem(course.id, module1.id, teacher, assignment1.name, ModuleItemTypes.ASSIGNMENT.stringVal, assignment1.id.toString())
+ Log.d(PREPARATION_TAG,"Associate '${assignment1.name}' assignment with '${module1.name}' module.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module1.id, assignment1.name, ModuleItemTypes.ASSIGNMENT.stringVal, contentId = assignment1.id.toString())
- Log.d(PREPARATION_TAG,"Associate ${quiz1.title} quiz with ${module1.name} module.")
- createModuleItem(course.id, module1.id, teacher, quiz1.title, ModuleItemTypes.QUIZ.stringVal, quiz1.id.toString())
+ Log.d(PREPARATION_TAG,"Associate '${quiz1.title}' quiz with '${module1.name}' module.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module1.id, quiz1.title, ModuleItemTypes.QUIZ.stringVal, contentId = quiz1.id.toString())
- Log.d(PREPARATION_TAG,"Associate ${assignment2.name} assignment with ${module2.name} module.")
- createModuleItem(course.id, module2.id, teacher, assignment2.name, ModuleItemTypes.ASSIGNMENT.stringVal, assignment2.id.toString())
+ Log.d(PREPARATION_TAG,"Associate '${assignment2.name}' assignment with '${module2.name}' module.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module2.id, assignment2.name, ModuleItemTypes.ASSIGNMENT.stringVal, contentId = assignment2.id.toString())
- Log.d(PREPARATION_TAG,"Associate ${page1.title} page with ${module2.name} module.")
- createModuleItem(course.id, module2.id, teacher, page1.title, ModuleItemTypes.PAGE.stringVal, null, page1.url)
+ Log.d(PREPARATION_TAG,"Associate '${page1.title}' page with '${module2.name}' module.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module2.id, page1.title, ModuleItemTypes.PAGE.stringVal, pageUrl = page1.url)
- Log.d(PREPARATION_TAG,"Associate ${discussionTopic1.title} discussion topic with ${module2.name} module.")
- createModuleItem(course.id, module2.id, teacher, discussionTopic1.title, ModuleItemTypes.DISCUSSION.stringVal, discussionTopic1.id.toString())
+ Log.d(PREPARATION_TAG,"Associate '${discussionTopic1.title}' discussion topic with '${module2.name}' module.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module2.id, discussionTopic1.title, ModuleItemTypes.DISCUSSION.stringVal, contentId = discussionTopic1.id.toString())
Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
tokenLogin(student)
dashboardPage.waitForRender()
- Log.d(STEP_TAG,"Assert that ${course.name} course is displayed.")
+ Log.d(STEP_TAG,"Assert that '${course.name}' course is displayed.")
dashboardPage.assertDisplaysCourse(course)
- Log.d(STEP_TAG,"Select ${course.name} course.")
+ Log.d(STEP_TAG,"Select '${course.name}' course.")
dashboardPage.selectCourse(course)
Log.d(STEP_TAG,"Assert that there are no modules displayed yet because there are not published. Assert that the 'Modules' Tab is not displayed as well.")
@@ -117,11 +116,11 @@ class ModulesE2ETest: StudentTest() {
Log.d(STEP_TAG,"Navigate back to Course Browser Page.")
Espresso.pressBack()
- Log.d(PREPARATION_TAG,"Publish ${module1.name} module.")
- publishModule(course, module1, teacher)
+ Log.d(PREPARATION_TAG,"Publish '${module1.name}' module.")
+ ModulesApi.updateModule(course.id, teacher.token, module1.id, published = true)
- Log.d(PREPARATION_TAG,"Publish ${module2.name} module.")
- publishModule(course, module2, teacher)
+ Log.d(PREPARATION_TAG,"Publish '${module2.name}' module.")
+ ModulesApi.updateModule(course.id, teacher.token, module2.id, published = true)
Log.d(STEP_TAG,"Refresh the page. Assert that the 'Modules' Tab is displayed.")
courseBrowserPage.refresh()
@@ -130,13 +129,13 @@ class ModulesE2ETest: StudentTest() {
Log.d(STEP_TAG,"Navigate to Modules Page.")
courseBrowserPage.selectModules()
- Log.d(STEP_TAG,"Assert that ${module1.name} module is displayed with the following items: ${assignment1.name} assignment, ${quiz1.title} quiz.")
+ Log.d(STEP_TAG,"Assert that '${module1.name}' module is displayed with the following items: '${assignment1.name}' assignment, '${quiz1.title}' quiz.")
modulesPage.assertModuleDisplayed(module1)
modulesPage.assertModuleItemDisplayed(module1, assignment1.name)
modulesPage.assertModuleItemDisplayed(module1, quiz1.title)
- Log.d(STEP_TAG,"Assert that ${module2.name} module is displayed with the following items: ${assignment2.name} assignment," +
- " ${page1.title} page, ${discussionTopic1.title} discussion topic.")
+ Log.d(STEP_TAG,"Assert that '${module2.name}' module is displayed with the following items: '${assignment2.name}' assignment," +
+ " '${page1.title}' page, '${discussionTopic1.title}' discussion topic.")
modulesPage.assertModuleDisplayed(module2)
modulesPage.assertModuleItemDisplayed(module2, assignment2.name)
modulesPage.assertModuleItemDisplayed(module2, page1.title)
@@ -150,97 +149,9 @@ class ModulesE2ETest: StudentTest() {
modulesPage.clickOnModuleExpandCollapseIcon(module2.name)
modulesPage.assertModulesAndItemsCount(7) // 2 modules titles, 2 module items in first module, 3 items in second module
- Log.d(STEP_TAG, "Assert that ${assignment1.name} module item is displayed and open it. Assert that the Assignment Details page is displayed with the corresponding assignment title.")
+ Log.d(STEP_TAG, "Assert that '${assignment1.name}' module item is displayed and open it. Assert that the Assignment Details page is displayed with the corresponding assignment title.")
modulesPage.assertAndClickModuleItem(module1.name, assignment1.name, true)
assignmentDetailsPage.assertPageObjects()
assignmentDetailsPage.assertAssignmentTitle(assignment1.name)
}
-
- private fun publishModule(
- course: CourseApiModel,
- module1: ModuleApiModel,
- teacher: CanvasUserApiModel
- ) {
- ModulesApi.updateModule(
- courseId = course.id,
- id = module1.id,
- published = true,
- teacherToken = teacher.token
- )
- }
-
- private fun createModuleItem(
- courseId: Long,
- moduleId: Long,
- teacher: CanvasUserApiModel,
- title: String,
- moduleItemType: String,
- contentId: String?,
- pageUrl: String? = null
- ) {
- ModulesApi.createModuleItem(
- courseId = courseId,
- moduleId = moduleId,
- teacherToken = teacher.token,
- title = title,
- type = moduleItemType,
- contentId = contentId,
- pageUrl = pageUrl
- )
- }
-
- private fun createModule(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) = ModulesApi.createModule(
- courseId = course.id,
- teacherToken = teacher.token,
- unlockAt = null
- )
-
- private fun createDiscussion(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) = DiscussionTopicsApi.createDiscussion(
- courseId = course.id,
- token = teacher.token
- )
-
- private fun createCoursePage(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) = PagesApi.createCoursePage(
- courseId = course.id,
- published = true,
- frontPage = false,
- token = teacher.token
- )
-
- private fun createQuiz(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) = QuizzesApi.createQuiz(
- QuizzesApi.CreateQuizRequest(
- courseId = course.id,
- withDescription = true,
- dueAt = 3.days.fromNow.iso8601,
- token = teacher.token,
- published = true
- )
- )
-
- private fun createAssignment(
- course: CourseApiModel,
- withDescription: Boolean,
- teacher: CanvasUserApiModel,
- dueAt: String
- ) = AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = course.id,
- withDescription = withDescription,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- teacherToken = teacher.token,
- dueAt = dueAt
- )
- )
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt
index 83723950af..5278f57e4e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt
@@ -21,16 +21,12 @@ import androidx.test.espresso.NoMatchingViewException
import com.instructure.canvas.espresso.E2E
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.Priority
-import com.instructure.canvas.espresso.ReleaseExclude
import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvas.espresso.refresh
import com.instructure.dataseeding.api.AssignmentsApi
import com.instructure.dataseeding.api.QuizzesApi
import com.instructure.dataseeding.api.SubmissionsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.GradingType
import com.instructure.dataseeding.model.QuizAnswer
import com.instructure.dataseeding.model.QuizQuestion
@@ -47,11 +43,11 @@ import java.lang.Thread.sleep
@HiltAndroidTest
class NotificationsE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
- @ReleaseExclude("The notifications API sometimes is slow and the test is breaking because the notifications aren't show up in time.")
@E2E
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.E2E)
@@ -63,10 +59,10 @@ class NotificationsE2ETest : StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seed an assignment for ${course.name} course.")
- val testAssignment = createAssignment(course, teacher)
+ Log.d(PREPARATION_TAG,"Seed an assignment for '${course.name}' course.")
+ val testAssignment = 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(PREPARATION_TAG,"Seed a quiz for ${course.name} course with some questions.")
+ Log.d(PREPARATION_TAG,"Seed a quiz for '${course.name}' course with some questions.")
val quizQuestions = makeQuizQuestions()
Log.d(PREPARATION_TAG,"Create and publish a quiz with the previously seeded questions.")
@@ -111,11 +107,11 @@ class NotificationsE2ETest : StudentTest() {
run submitAndGradeRepeat@{
repeat(10) {
try {
- Log.d(PREPARATION_TAG, "Submit ${testAssignment.name} assignment with student: ${student.name}.")
- submitAssignment(course, testAssignment, student)
+ Log.d(PREPARATION_TAG, "Submit '${testAssignment.name}' assignment with student: '${student.name}'.")
+ SubmissionsApi.submitCourseAssignment(course.id, student.token, testAssignment.id, SubmissionType.ONLINE_TEXT_ENTRY)
- Log.d(PREPARATION_TAG, "Grade the submission of ${student.name} student for assignment: ${testAssignment.name}.")
- gradeSubmission(teacher, course, testAssignment, student)
+ Log.d(PREPARATION_TAG, "Grade the submission of '${student.name}' student for assignment: '${testAssignment.name}'.")
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, testAssignment.id, student.id, postedGrade = "13")
Log.d(STEP_TAG, "Refresh the Notifications Page. Assert that there is a notification about the submission grading appearing.")
sleep(3000) //Let the submission api do it's job
@@ -129,36 +125,6 @@ class NotificationsE2ETest : StudentTest() {
}
}
- private fun gradeSubmission(
- teacher: CanvasUserApiModel,
- course: CourseApiModel,
- testAssignment: AssignmentApiModel,
- student: CanvasUserApiModel
- ) {
- SubmissionsApi.gradeSubmission(
- teacherToken = teacher.token,
- courseId = course.id,
- assignmentId = testAssignment.id,
- studentId = student.id,
- postedGrade = "13",
- excused = false
- )
- }
-
- private fun submitAssignment(
- course: CourseApiModel,
- testAssignment: AssignmentApiModel,
- student: CanvasUserApiModel
- ) {
- SubmissionsApi.submitCourseAssignment(
- submissionType = SubmissionType.ONLINE_TEXT_ENTRY,
- courseId = course.id,
- assignmentId = testAssignment.id,
- studentToken = student.token,
- fileIds = emptyList().toMutableList()
- )
- }
-
private fun makeQuizQuestions() = listOf(
QuizQuestion(
questionText = "What's your favorite color?",
@@ -181,21 +147,4 @@ class NotificationsE2ETest : StudentTest() {
)
)
)
-
- private fun createAssignment(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) : AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = course.id,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- gradingType = GradingType.POINTS,
- teacherToken = teacher.token,
- pointsPossible = 15.0,
- dueAt = 1.days.fromNow.iso8601
- )
- )
- }
-
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt
index 584fa2f705..8903a2c4cb 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt
@@ -25,9 +25,6 @@ import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.dataseeding.api.PagesApi
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
-import com.instructure.dataseeding.util.Randomizer
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
@@ -53,16 +50,16 @@ class PagesE2ETest: StudentTest() {
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Seed an UNPUBLISHED page for '${course.name}' course.")
- val pageUnpublished = createCoursePage(course, teacher, published = false, frontPage = false)
+ val pageUnpublished = PagesApi.createCoursePage(course.id, teacher.token, published = false)
Log.d(PREPARATION_TAG,"Seed a PUBLISHED page for '${course.name}' course.")
- val pagePublished = createCoursePage(course, teacher, published = true, frontPage = false, editingRoles = "teachers,students", body = "")
+ val pagePublished = PagesApi.createCoursePage(course.id, teacher.token, editingRoles = "teachers,students", body = "")
Log.d(PREPARATION_TAG,"Seed a PUBLISHED, but NOT editable page for '${course.name}' course.")
- val pageNotEditable = createCoursePage(course, teacher, published = true, frontPage = false, body = "")
+ val pageNotEditable = PagesApi.createCoursePage(course.id, teacher.token, body = "")
Log.d(PREPARATION_TAG,"Seed a PUBLISHED, FRONT page for '${course.name}' course.")
- val pagePublishedFront = createCoursePage(course, teacher, published = true, frontPage = true, editingRoles = "public", body = "")
+ val pagePublishedFront = PagesApi.createCoursePage(course.id, teacher.token, frontPage = true, editingRoles = "public", body = "")
Log.d(STEP_TAG,"Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
@@ -137,20 +134,4 @@ class PagesE2ETest: StudentTest() {
Log.d(STEP_TAG, "Assert that the new, edited text is displayed in the page body.")
canvasWebViewPage.runTextChecks(WebViewTextCheck(Locator.ID, "header1", "Front Page Text Mod"))
}
-
- private fun createCoursePage(
- course: CourseApiModel,
- teacher: CanvasUserApiModel,
- published: Boolean,
- frontPage: Boolean,
- editingRoles: String? = null,
- body: String = Randomizer.randomPageBody()
- ) = PagesApi.createCoursePage(
- courseId = course.id,
- published = published,
- frontPage = frontPage,
- editingRoles = editingRoles,
- token = teacher.token,
- body = body
- )
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt
index 89709d0222..4a05b67783 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt
@@ -51,7 +51,7 @@ class PeopleE2ETest : StudentTest() {
tokenLogin(student1)
dashboardPage.waitForRender()
- Log.d(STEP_TAG,"Navigate to ${course.name} course's People Page.")
+ Log.d(STEP_TAG,"Navigate to '${course.name}' course's People Page.")
dashboardPage.selectCourse(course)
courseBrowserPage.selectPeople()
@@ -70,7 +70,7 @@ class PeopleE2ETest : StudentTest() {
peopleListPage.assertPersonListed(student2)
peopleListPage.assertPeopleCount(3)
- Log.d(STEP_TAG,"Select ${student2.name} student and assert if we are landing on the Person Details Page.")
+ Log.d(STEP_TAG,"Select ${student2.name} student and assert if we are landing on the Person Details Page. Assert that the Person Details page's information (user name, role, and picture) are displayed.")
peopleListPage.selectPerson(student2)
personDetailsPage.assertPageObjects()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt
index f6094a40ba..e1073754a1 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt
@@ -36,9 +36,6 @@ import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvas.espresso.containsTextCaseInsensitive
import com.instructure.canvas.espresso.isElementDisplayed
import com.instructure.dataseeding.api.QuizzesApi
-import com.instructure.dataseeding.api.QuizzesApi.createAndPublishQuiz
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.QuizAnswer
import com.instructure.dataseeding.model.QuizQuestion
import com.instructure.student.R
@@ -57,7 +54,7 @@ class QuizzesE2ETest: StudentTest() {
override fun enableAndConfigureAccessibilityChecks() = Unit
- // Fairly basic test of webview-based quizzes. Seeds/takes a quiz with two multiple-choice
+ // Fairly basic test of web view-based quizzes. Seeds/takes a quiz with two multiple-choice
// questions.
//
// STUBBING THIS OUT. Usually passes locally, but I can't get a simple webClick() to work on FTL.
@@ -75,13 +72,13 @@ class QuizzesE2ETest: StudentTest() {
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Seed a quiz for ${course.name} course.")
- val quizUnpublished = createQuiz(course, teacher, withDescription = true, published = false)
+ val quizUnpublished = QuizzesApi.createQuiz(course.id, teacher.token, published = false)
Log.d(PREPARATION_TAG,"Seed another quiz for ${course.name} with some questions.")
val quizQuestions = makeQuizQuestions()
Log.d(PREPARATION_TAG,"Publish the previously seeded quiz.")
- val quizPublished = createAndPublishQuiz(course.id, teacher.token, quizQuestions)
+ val quizPublished = QuizzesApi.createAndPublishQuiz(course.id, teacher.token, quizQuestions)
Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.")
tokenLogin(student)
@@ -181,6 +178,7 @@ class QuizzesE2ETest: StudentTest() {
Log.d(STEP_TAG,"Select ${quizPublished.title} quiz.")
quizListPage.selectQuiz(quizPublished)
+ sleep(5000)
Log.d(STEP_TAG,"Assert (on web) that the ${quizPublished.title} quiz now has a history.")
onWebView(withId(R.id.contentWebView))
.withElement(findElement(Locator.ID, "quiz-submission-version-table"))
@@ -204,20 +202,6 @@ class QuizzesE2ETest: StudentTest() {
}
- private fun createQuiz(
- course: CourseApiModel,
- teacher: CanvasUserApiModel,
- withDescription: Boolean,
- published: Boolean,
- ) = QuizzesApi.createQuiz(
- QuizzesApi.CreateQuizRequest(
- courseId = course.id,
- withDescription = withDescription,
- published = published,
- token = teacher.token
- )
- )
-
private fun makeQuizQuestions() = listOf(
QuizQuestion(
questionText = "What's your favorite color?",
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt
index babe1f110d..ca431ea2ab 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt
@@ -66,10 +66,10 @@ class SettingsE2ETest : StudentTest() {
profileSettingsPage.assertPageObjects()
val newUserName = "John Doe"
- Log.d(STEP_TAG, "Edit username to: $newUserName. Click on 'Save' button.")
+ Log.d(STEP_TAG, "Edit username to: '$newUserName'. Click on 'Save' button.")
profileSettingsPage.changeUserNameTo(newUserName)
- Log.d(STEP_TAG, "Navigate back to Dashboard Page. Assert that the username has been changed to $newUserName.")
+ Log.d(STEP_TAG, "Navigate back to Dashboard Page. Assert that the username has been changed to '$newUserName'.")
ViewUtils.pressBackButton(2)
leftSideNavigationDrawerPage.assertUserLoggedIn(newUserName)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt
index 8df03a41d9..0d611d4474 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt
@@ -25,9 +25,6 @@ import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import com.instructure.canvas.espresso.E2E
import com.instructure.dataseeding.api.AssignmentsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.GradingType
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.days
@@ -61,11 +58,11 @@ class ShareExtensionE2ETest: StudentTest() {
val course = data.coursesList[0]
val teacher = data.teachersList[0]
- Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.")
- val testAssignmentOne = createAssignment(course, teacher, 1.days.fromNow.iso8601, 15.0)
+ Log.d(PREPARATION_TAG,"Seeding 'File upload' assignment for ${course.name} course.")
+ val testAssignmentOne = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD))
- Log.d(PREPARATION_TAG,"Seeding another 'Text Entry' assignment for ${course.name} course.")
- createAssignment(course, teacher, 1.days.fromNow.iso8601, 30.0)
+ Log.d(PREPARATION_TAG,"Seeding another 'File upload' assignment for ${course.name} course.")
+ AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 30.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD))
Log.d(PREPARATION_TAG, "Get the device to be able to perform app-independent actions on it.")
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@@ -205,24 +202,6 @@ class ShareExtensionE2ETest: StudentTest() {
fileListPage.assertItemDisplayed(jpgTestFileName)
}
- private fun createAssignment(
- course: CourseApiModel,
- teacher: CanvasUserApiModel,
- dueAt: String,
- pointsPossible: Double
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = course.id,
- submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD),
- gradingType = GradingType.POINTS,
- teacherToken = teacher.token,
- pointsPossible = pointsPossible,
- dueAt = dueAt
- )
- )
- }
-
private fun shareMultipleFiles(uris: ArrayList) {
val intent = Intent().apply {
action = Intent.ACTION_SEND_MULTIPLE
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt
index 8db435bf9b..245e3c135d 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt
@@ -24,8 +24,6 @@ import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.dataseeding.api.AssignmentsApi
import com.instructure.dataseeding.api.QuizzesApi
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
@@ -55,9 +53,9 @@ class SyllabusE2ETest: StudentTest() {
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.")
+ Log.d(STEP_TAG,"Wait for the Dashboard Page to be rendered. Select '${course.name}' course.")
+ dashboardPage.waitForRender()
dashboardPage.selectCourse(course)
Log.d(STEP_TAG,"Navigate to Syllabus Page. Assert that the syllabus body string is displayed, and there are no tabs yet.")
@@ -65,42 +63,16 @@ class SyllabusE2ETest: StudentTest() {
syllabusPage.assertNoTabs()
syllabusPage.assertSyllabusBody("this is the syllabus body")
- Log.d(PREPARATION_TAG,"Seed an assignment for ${course.name} course.")
- val assignment = createAssignment(course, teacher)
+ Log.d(PREPARATION_TAG,"Seed an assignment for '${course.name}' course.")
+ val assignment = AssignmentsApi.createAssignment(course.id, teacher.token, submissionTypes = listOf(SubmissionType.ON_PAPER), withDescription = true, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601)
- Log.d(PREPARATION_TAG,"Seed a quiz for ${course.name} course.")
- val quiz = createQuiz(course, teacher)
+ Log.d(PREPARATION_TAG,"Seed a quiz for '${course.name}' course.")
+ val quiz = QuizzesApi.createQuiz(course.id, teacher.token, dueAt = 2.days.fromNow.iso8601)
- Log.d(STEP_TAG,"Refresh the page. Navigate to 'Summary' tab. Assert that all of the items, so ${assignment.name} assignment and ${quiz.title} quiz are displayed.")
+ Log.d(STEP_TAG,"Refresh the page. Navigate to 'Summary' tab. Assert that all of the items, so '${assignment.name}' assignment and '${quiz.title}' quiz are displayed.")
syllabusPage.refresh()
syllabusPage.selectSummaryTab()
syllabusPage.assertItemDisplayed(assignment.name)
syllabusPage.assertItemDisplayed(quiz.title)
}
-
- private fun createQuiz(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) = QuizzesApi.createQuiz(
- QuizzesApi.CreateQuizRequest(
- courseId = course.id,
- withDescription = true,
- published = true,
- token = teacher.token,
- dueAt = 2.days.fromNow.iso8601
- )
- )
-
- private fun createAssignment(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) = AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = course.id,
- teacherToken = teacher.token,
- submissionTypes = listOf(SubmissionType.ON_PAPER),
- dueAt = 1.days.fromNow.iso8601,
- withDescription = true
- )
- )
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt
index 32b3540313..08e00b02e0 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt
@@ -3,23 +3,20 @@ package com.instructure.student.ui.e2e
import android.util.Log
import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
+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.refresh
import com.instructure.dataseeding.api.AssignmentsApi
import com.instructure.dataseeding.api.QuizzesApi
import com.instructure.dataseeding.api.SubmissionsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.GradingType
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.retry
-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.espresso.retryWithIncreasingDelay
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedAssignments
import com.instructure.student.ui.utils.seedData
@@ -46,10 +43,10 @@ class TodoE2ETest: StudentTest() {
val course = data.coursesList[0]
val favoriteCourse = data.coursesList[1]
- Log.d(PREPARATION_TAG,"Seed an assignment for ${course.name} course with tomorrow due date.")
- val testAssignment = createAssignment(course, teacher)
+ Log.d(PREPARATION_TAG,"Seed an assignment for '${course.name}' course with tomorrow due date.")
+ val testAssignment = 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(PREPARATION_TAG,"Seed another assignment for ${course.name} course with 7 days from now due date.")
+ Log.d(PREPARATION_TAG,"Seed another assignment for '${course.name}' course with 7 days from now due date.")
val seededAssignments2 = seedAssignments(
courseId = course.id,
teacherToken = teacher.token,
@@ -58,38 +55,35 @@ class TodoE2ETest: StudentTest() {
val borderDateAssignment = seededAssignments2[0] //We show items in the to do section which are within 7 days.
- Log.d(PREPARATION_TAG,"Seed a quiz for ${course.name} course with tomorrow due date.")
- val quiz = createQuiz(course, teacher, 1.days.fromNow.iso8601)
+ Log.d(PREPARATION_TAG,"Seed a quiz for '${course.name}' course with tomorrow due date.")
+ val quiz = QuizzesApi.createQuiz(course.id, teacher.token, dueAt = 1.days.fromNow.iso8601)
- Log.d(PREPARATION_TAG,"Seed another quiz for ${course.name} course with 8 days from now due date..")
- val tooFarAwayQuiz = createQuiz(course, teacher, 8.days.fromNow.iso8601)
+ Log.d(PREPARATION_TAG,"Seed another quiz for '${course.name}' course with 8 days from now due date..")
+ val tooFarAwayQuiz = QuizzesApi.createQuiz(course.id, teacher.token, dueAt = 8.days.fromNow.iso8601)
Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.")
tokenLogin(student)
- dashboardPage.waitForRender()
- Log.d(STEP_TAG,"Navigate to 'To Do' Page via bottom-menu.")
+ Log.d(STEP_TAG,"Wait for the Dashboard Page to be rendered. Navigate to 'To Do' Page via bottom-menu.")
+ dashboardPage.waitForRender()
dashboardPage.clickTodoTab()
- Log.d(STEP_TAG,"Assert that ${testAssignment.name} assignment is displayed and ${borderDateAssignment.name} is displayed because it's 7 days away from now..")
- Log.d(STEP_TAG,"Assert that ${quiz.title} quiz is displayed and ${tooFarAwayQuiz.title} quiz is not displayed because it's end date is more than a week away..")
- retry(times = 5, delay = 3000, catchBlock = { refresh() } ) {
+ Log.d(STEP_TAG,"Assert that '${testAssignment.name}' assignment is displayed and '${borderDateAssignment.name}' assignment is displayed because it's 7 days away from now.")
+ Log.d(STEP_TAG,"Assert that '${quiz.title}' quiz is displayed and '${tooFarAwayQuiz.title}' quiz is not displayed because it's end date is more than a week away.")
+ retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() } ) {
todoPage.assertAssignmentDisplayed(testAssignment)
todoPage.assertAssignmentDisplayed(borderDateAssignment)
todoPage.assertQuizDisplayed(quiz)
todoPage.assertQuizNotDisplayed(tooFarAwayQuiz)
}
- Log.d(PREPARATION_TAG,"Submit ${testAssignment.name} assignment for ${student.name} student.")
- SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest(
- assignmentId = testAssignment.id,
- courseId = course.id,
- studentToken = student.token,
+ Log.d(PREPARATION_TAG,"Submit' ${testAssignment.name}' assignment for ${student.name} student.")
+ SubmissionsApi.seedAssignmentSubmission(course.id, student.token, testAssignment.id,
submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(
amount = 1,
submissionType = SubmissionType.ONLINE_TEXT_ENTRY
))
- ))
+ )
Log.d(STEP_TAG, "Refresh the 'To Do' Page.")
refresh()
@@ -115,10 +109,10 @@ class TodoE2ETest: StudentTest() {
todoPage.assertAssignmentNotDisplayed(testAssignment)
todoPage.assertQuizNotDisplayed(tooFarAwayQuiz)
- Log.d(PREPARATION_TAG,"Seed an assignment for ${favoriteCourse.name} course with tomorrow due date.")
- val favoriteCourseAssignment = createAssignment(favoriteCourse, teacher)
+ Log.d(PREPARATION_TAG,"Seed an assignment for '${favoriteCourse.name}' course with tomorrow due date.")
+ val favoriteCourseAssignment = AssignmentsApi.createAssignment(favoriteCourse.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
- Log.d(STEP_TAG, "Navigate back to the Dashboard Page. Open ${favoriteCourse.name} course. Mark it as favorite.")
+ Log.d(STEP_TAG, "Navigate back to the Dashboard Page. Open '${favoriteCourse.name}' course. Mark it as favorite.")
Espresso.pressBack()
dashboardPage.openAllCoursesPage()
allCoursesPage.favoriteCourse(favoriteCourse.name)
@@ -141,34 +135,4 @@ class TodoE2ETest: StudentTest() {
todoPage.assertQuizNotDisplayed(quiz)
todoPage.assertQuizNotDisplayed(tooFarAwayQuiz)
}
-
- private fun createQuiz(
- course: CourseApiModel,
- teacher: CanvasUserApiModel,
- dueAt: String
- ) = QuizzesApi.createQuiz(
- QuizzesApi.CreateQuizRequest(
- courseId = course.id,
- withDescription = true,
- published = true,
- token = teacher.token,
- dueAt = dueAt
- )
- )
-
- private fun createAssignment(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = course.id,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- gradingType = GradingType.POINTS,
- teacherToken = teacher.token,
- pointsPossible = 15.0,
- dueAt = 1.days.fromNow.iso8601
- )
- )
- }
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt
index 088b6e0858..6714978284 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt
@@ -21,16 +21,17 @@ import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
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.canvasapi2.utils.toApiString
import com.instructure.dataseeding.api.AssignmentsApi
import com.instructure.dataseeding.api.GradingPeriodsApi
import com.instructure.dataseeding.api.SubmissionsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
import com.instructure.dataseeding.model.GradingType
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.page.getStringFromResource
import com.instructure.student.R
import com.instructure.student.ui.pages.ElementaryDashboardPage
@@ -39,17 +40,17 @@ import com.instructure.student.ui.utils.seedDataForK5
import com.instructure.student.ui.utils.tokenLoginElementary
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
-import java.util.*
@HiltAndroidTest
class GradesElementaryE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@E2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.E2E, SecondaryFeatureCategory.K5_GRADES)
fun gradesE2ETest() {
Log.d(PREPARATION_TAG,"Seeding data for K5 sub-account.")
@@ -65,20 +66,19 @@ class GradesElementaryE2ETest : StudentTest() {
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val nonHomeroomCourses = data.coursesList.filter { !it.homeroomCourse }
- val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
val testGradingPeriodListApiModel = GradingPeriodsApi.getGradingPeriodsOfCourse(nonHomeroomCourses[0].id)
- Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${nonHomeroomCourses[1].name} course.")
- val testAssignment = createAssignment(nonHomeroomCourses[1].id, teacher, calendar, GradingType.PERCENT, 100.0)
+ Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for '${nonHomeroomCourses[1].name}' course.")
+ val testAssignment = AssignmentsApi.createAssignment(nonHomeroomCourses[1].id, teacher.token, gradingType = GradingType.PERCENT, pointsPossible = 100.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
- Log.d(PREPARATION_TAG,"Seeding another 'Text Entry' assignment for ${nonHomeroomCourses[0].name} course.")
- val testAssignment2 = createAssignment(nonHomeroomCourses[0].id, teacher, calendar, GradingType.LETTER_GRADE, 100.0)
+ Log.d(PREPARATION_TAG,"Seeding another 'Text Entry' assignment for '${nonHomeroomCourses[0].name}' course.")
+ val testAssignment2 = AssignmentsApi.createAssignment(nonHomeroomCourses[0].id, teacher.token, gradingType = GradingType.LETTER_GRADE, pointsPossible = 100.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
- Log.d(PREPARATION_TAG,"Grade the previously seeded submission for ${nonHomeroomCourses[1].name} assignment.")
- gradeSubmission(teacher,nonHomeroomCourses[1].id, student, testAssignment.id, "9")
+ Log.d(PREPARATION_TAG,"Grade the previously seeded submission for '${nonHomeroomCourses[1].name}' assignment.")
+ SubmissionsApi.gradeSubmission(teacher.token, nonHomeroomCourses[1].id, testAssignment.id, student.id, postedGrade = "9")
- Log.d(PREPARATION_TAG,"Grade the previously seeded submission for ${nonHomeroomCourses[0].name} assignment.")
- gradeSubmission(teacher, nonHomeroomCourses[0].id, student, testAssignment2.id, "A-")
+ Log.d(PREPARATION_TAG,"Grade the previously seeded submission for '${nonHomeroomCourses[0].name}' assignment.")
+ SubmissionsApi.gradeSubmission(teacher.token, nonHomeroomCourses[0].id, testAssignment2.id, student.id, postedGrade = "A-")
Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
tokenLoginElementary(student)
@@ -94,16 +94,17 @@ class GradesElementaryE2ETest : StudentTest() {
gradesPage.assertCourseShownWithGrades(nonHomeroomCourses[1].name, "9%")
gradesPage.assertCourseShownWithGrades(nonHomeroomCourses[2].name, "Not Graded")
- Log.d(PREPARATION_TAG,"Grade the previously seeded submission for ${testAssignment2.name} assignment.")
- gradeSubmission(teacher,nonHomeroomCourses[0].id, student, testAssignment2.id, "C-")
+ Log.d(PREPARATION_TAG,"Grade the previously seeded submission for '${testAssignment2.name}' assignment.")
+ SubmissionsApi.gradeSubmission(teacher.token, nonHomeroomCourses[0].id, testAssignment2.id, student.id, postedGrade = "C-")
Thread.sleep(5000) //This time is needed here to let the SubMissionApi does it's job.
- Log.d(STEP_TAG, "Refresh Grades Elementary Page. Assert that the previously graded, ${testAssignment2.name} assignment's grade has been changed, but only that one.")
+ Log.d(STEP_TAG, "Refresh Grades Elementary Page. Assert that the previously graded, '${testAssignment2.name}' assignment's grade has been changed, but only that one.")
gradesPage.refresh()
Thread.sleep(5000) //We need to wait here because sometimes if we refresh the page fastly, the old grade will be seen.
gradesPage.assertCourseShownWithGrades(nonHomeroomCourses[0].name, "73%")
gradesPage.assertCourseShownWithGrades(nonHomeroomCourses[1].name, "9%")
+ gradesPage.assertCourseShownWithGrades(nonHomeroomCourses[2].name, "Not Graded")
Log.d(STEP_TAG, "Change 'Current Grading Period' to '${testGradingPeriodListApiModel.gradingPeriods[0].title}'.")
gradesPage.assertSelectedGradingPeriod(gradesPage.getStringFromResource(R.string.currentGradingPeriod))
@@ -111,50 +112,14 @@ class GradesElementaryE2ETest : StudentTest() {
gradesPage.clickGradingPeriodSelector()
gradesPage.selectGradingPeriod(testGradingPeriodListApiModel.gradingPeriods[0].title)
- Log.d(STEP_TAG, "Checking if a course's grades page is displayed after clicking on a course row on elementary grades page. Assert that we have left the grades elementary page. We are asserting this because in beta environment, subject page's not always available for k5 user.")
+ Log.d(STEP_TAG, "Checking if a course's grades page is displayed after clicking on a course row on elementary grades page." +
+ "Assert that we have left the grades elementary page. We are asserting this because in beta environment, subject page's not always available for k5 user.")
gradesPage.clickGradeRow(nonHomeroomCourses[0].name)
gradesPage.assertCourseNotDisplayed(nonHomeroomCourses[0].name)
Log.d(STEP_TAG, "Navigate back to Grades Elementary Page and assert it's displayed.")
Espresso.pressBack()
gradesPage.assertPageObjects()
-
- }
-
- private fun createAssignment(
- courseId: Long,
- teacher: CanvasUserApiModel,
- calendar: Calendar,
- gradingType: GradingType,
- pointsPossible: Double
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = courseId,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- gradingType = gradingType,
- teacherToken = teacher.token,
- pointsPossible = pointsPossible,
- dueAt = calendar.time.toApiString()
- )
- )
- }
-
- private fun gradeSubmission(
- teacher: CanvasUserApiModel,
- courseId: Long,
- student: CanvasUserApiModel,
- assignmentId: Long,
- postedGrade: String
- ) {
- SubmissionsApi.gradeSubmission(
- teacherToken = teacher.token,
- courseId = courseId,
- assignmentId = assignmentId,
- studentId = student.id,
- postedGrade = postedGrade,
- excused = false
- )
}
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt
index 6c6703011d..b9d1d7d138 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt
@@ -21,11 +21,10 @@ import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
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.dataseeding.api.AssignmentsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
import com.instructure.dataseeding.model.GradingType
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.ago
@@ -44,13 +43,14 @@ import org.threeten.bp.format.DateTimeFormatter
@HiltAndroidTest
class HomeroomE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@E2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.E2E, SecondaryFeatureCategory.HOMEROOM)
fun homeroomE2ETest() {
Log.d(PREPARATION_TAG,"Seeding data for K5 sub-account.")
@@ -68,11 +68,11 @@ class HomeroomE2ETest : StudentTest() {
val homeroomAnnouncement = data.announcementsList[0]
val nonHomeroomCourses = data.coursesList.filter { !it.homeroomCourse }
- Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${nonHomeroomCourses[2].name} course.")
- val testAssignment = createAssignment(nonHomeroomCourses[2].id, teacher, GradingType.LETTER_GRADE, 100.0, OffsetDateTime.now().plusHours(1).format(DateTimeFormatter.ISO_DATE_TIME))
+ Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for '${nonHomeroomCourses[2].name}' course.")
+ val testAssignment = AssignmentsApi.createAssignment(nonHomeroomCourses[2].id, teacher.token, gradingType = GradingType.LETTER_GRADE, pointsPossible = 100.0, dueAt = OffsetDateTime.now().plusHours(1).format(DateTimeFormatter.ISO_DATE_TIME), submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
Log.d(PREPARATION_TAG,"Seeding 'Text Entry' MISSING assignment for ${nonHomeroomCourses[2].name} course.")
- val testAssignmentMissing = createAssignment(nonHomeroomCourses[2].id, teacher, GradingType.PERCENT, 100.0, 3.days.ago.iso8601)
+ val testAssignmentMissing = AssignmentsApi.createAssignment(nonHomeroomCourses[2].id, teacher.token, gradingType = GradingType.PERCENT, pointsPossible = 100.0, dueAt = 3.days.ago.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
tokenLoginElementary(student)
@@ -81,10 +81,10 @@ class HomeroomE2ETest : StudentTest() {
Log.d(STEP_TAG, "Navigate to K5 Important Dates Page and assert it is loaded.")
elementaryDashboardPage.selectTab(ElementaryDashboardPage.ElementaryTabType.HOMEROOM)
- Log.d(STEP_TAG, "Assert that there is a welcome text with the student's shortname (${student.shortName}).")
+ Log.d(STEP_TAG, "Assert that there is a welcome text with the student's shortname: '${student.shortName}'.")
homeroomPage.assertWelcomeText(student.shortName)
- Log.d(STEP_TAG, "Assert that the ${homeroomAnnouncement.title} announcement (which belongs to ${homeroomCourse.name} homeroom course) is displayed.")
+ Log.d(STEP_TAG, "Assert that the '${homeroomAnnouncement.title}' announcement (which belongs to '${homeroomCourse.name}' homeroom course) is displayed.")
homeroomPage.assertAnnouncementDisplayed(homeroomCourse.name, homeroomAnnouncement.title, homeroomAnnouncement.message)
Log.d(STEP_TAG, "Assert that under the 'My Subject' section there are 3 items.")
@@ -92,7 +92,7 @@ class HomeroomE2ETest : StudentTest() {
Log.d(STEP_TAG, "Click on 'View Previous Announcements'." +
"Assert that the Announcement List Page is displayed" +
- "and the ${homeroomAnnouncement.title} announcement is displayed as well within the announcement list..")
+ "and the '${homeroomAnnouncement.title}' announcement is displayed as well within the announcement list..")
homeroomPage.clickOnViewPreviousAnnouncements()
announcementListPage.assertToolbarTitle()
announcementListPage.assertAnnouncementTitleVisible(homeroomAnnouncement.title)
@@ -103,7 +103,7 @@ class HomeroomE2ETest : StudentTest() {
elementaryDashboardPage.waitForRender()
for (i in 0 until nonHomeroomCourses.size - 1) {
- Log.d(STEP_TAG, "Assert that the ${nonHomeroomCourses[i].name} course is displayed with the announcements which belongs to it.")
+ Log.d(STEP_TAG, "Assert that the '${nonHomeroomCourses[i].name}' course is displayed with the announcements which belongs to it.")
homeroomPage.assertCourseDisplayed(
nonHomeroomCourses[i].name,
homeroomPage.getStringFromResource(R.string.nothingDueToday),
@@ -115,50 +115,30 @@ class HomeroomE2ETest : StudentTest() {
homeroomPage.assertPageObjects()
homeroomPage.assertToDoText("1 due today | 1 missing")
- Log.d(STEP_TAG, "Open ${nonHomeroomCourses[0].name} course." +
+ Log.d(STEP_TAG, "Open '${nonHomeroomCourses[0].name}' course." +
"Assert that the Course Details Page is displayed and the title is '${nonHomeroomCourses[0].name}' (the course's name).")
homeroomPage.openCourse(nonHomeroomCourses[0].name)
elementaryCoursePage.assertPageObjects()
elementaryCoursePage.assertTitleCorrect(nonHomeroomCourses[0].name)
- Log.d(STEP_TAG, "Navigate back to Homeroom Page. Open ${data.announcementsList[1].title} announcement by clicking on it.")
+ Log.d(STEP_TAG, "Navigate back to Homeroom Page. Open '${data.announcementsList[1].title}' announcement by clicking on it.")
Espresso.pressBack()
homeroomPage.assertPageObjects()
homeroomPage.openCourseAnnouncement(data.announcementsList[1].title)
- Log.d(STEP_TAG, "Assert that the ${data.announcementsList[1].title} announcement's details page is displayed.")
+ Log.d(STEP_TAG, "Assert that the '${data.announcementsList[1].title}' announcement's details page is displayed.")
discussionDetailsPage.assertTitleText(data.announcementsList[1].title)
- Log.d(STEP_TAG, "Navigate back to Homeroom Page. Open the Assignment List Page of ${nonHomeroomCourses[2].name} course.")
+ Log.d(STEP_TAG, "Navigate back to Homeroom Page. Open the Assignment List Page of '${nonHomeroomCourses[2].name}' course.")
Espresso.pressBack()
homeroomPage.assertPageObjects()
homeroomPage.openAssignments("1 due today | 1 missing")
- Log.d(STEP_TAG, "Assert that the Assignment list page of ${nonHomeroomCourses[2].name} course is loaded well" +
- "and the corresponding assignments (Not missing: ${testAssignment.name}, missing: ${testAssignmentMissing.name}) are displayed.")
+ Log.d(STEP_TAG, "Assert that the Assignment list page of '${nonHomeroomCourses[2].name}' course is loaded well" +
+ "and the corresponding assignments (Not missing: '${testAssignment.name}', missing: '${testAssignmentMissing.name}') are displayed.")
assignmentListPage.assertPageObjects()
assignmentListPage.assertHasAssignment(testAssignment)
assignmentListPage.assertHasAssignment(testAssignmentMissing)
}
-
-
- private fun createAssignment(
- courseId: Long,
- teacher: CanvasUserApiModel,
- gradingType: GradingType,
- pointsPossible: Double,
- dueAt: String
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = courseId,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- gradingType = gradingType,
- teacherToken = teacher.token,
- pointsPossible = pointsPossible,
- dueAt = dueAt
- )
- )
- }
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt
index d569f6c99b..3628506f0b 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt
@@ -21,14 +21,12 @@ import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
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.canvasapi2.utils.toDate
import com.instructure.dataseeding.api.AssignmentsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
import com.instructure.dataseeding.model.GradingType
-import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
@@ -43,13 +41,14 @@ import java.util.*
@HiltAndroidTest
class ImportantDatesE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@E2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.E2E, SecondaryFeatureCategory.IMPORTANT_DATES)
fun importantDatesE2ETest() {
Log.d(PREPARATION_TAG,"Seeding data for K5 sub-account.")
@@ -67,17 +66,17 @@ class ImportantDatesE2ETest : StudentTest() {
val elementaryCourse3 = data.coursesList[2]
val elementaryCourse4 = data.coursesList[3]
- Log.d(PREPARATION_TAG,"Seeding 'Text Entry' IMPORTANT assignment for ${elementaryCourse1.name} course.")
- val testAssignment1 = createAssignment(elementaryCourse1.id,teacher, GradingType.POINTS, 100.0, 3.days.fromNow.iso8601, importantDate = true)
+ Log.d(PREPARATION_TAG,"Seeding 'Text Entry' IMPORTANT assignment for '${elementaryCourse1.name}' course.")
+ val testAssignment1 = AssignmentsApi.createAssignment(elementaryCourse1.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 100.0, dueAt = 3.days.fromNow.iso8601, importantDate = true)
- Log.d(PREPARATION_TAG,"Seeding 'Text Entry' IMPORTANT assignment for ${elementaryCourse2.name} course.")
- val testAssignment2 = createAssignment(elementaryCourse2.id,teacher, GradingType.POINTS, 100.0, 4.days.fromNow.iso8601, importantDate = true)
+ Log.d(PREPARATION_TAG,"Seeding 'Text Entry' IMPORTANT assignment for '${elementaryCourse2.name}' course.")
+ val testAssignment2 = AssignmentsApi.createAssignment(elementaryCourse2.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 100.0, dueAt = 4.days.fromNow.iso8601, importantDate = true)
- Log.d(PREPARATION_TAG,"Seeding 'Text Entry' IMPORTANT assignment for ${elementaryCourse3.name} course.")
- val testAssignment3 = createAssignment(elementaryCourse3.id,teacher, GradingType.POINTS, 100.0, 4.days.fromNow.iso8601, importantDate = true)
+ Log.d(PREPARATION_TAG,"Seeding 'Text Entry' IMPORTANT assignment for '${elementaryCourse3.name}' course.")
+ val testAssignment3 = AssignmentsApi.createAssignment(elementaryCourse3.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 100.0, dueAt = 4.days.fromNow.iso8601, importantDate = true)
- Log.d(PREPARATION_TAG,"Seeding 'Text Entry' NOT IMPORTANT assignment for ${elementaryCourse4.name} course.")
- val testNotImportantAssignment = createAssignment(elementaryCourse4.id,teacher, GradingType.POINTS, 100.0, 4.days.fromNow.iso8601, importantDate = false)
+ Log.d(PREPARATION_TAG,"Seeding 'Text Entry' NOT IMPORTANT assignment for '${elementaryCourse4.name}' course.")
+ val testNotImportantAssignment = AssignmentsApi.createAssignment(elementaryCourse4.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 100.0, dueAt = 4.days.fromNow.iso8601, importantDate = false)
Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
tokenLoginElementary(student)
@@ -87,7 +86,7 @@ class ImportantDatesE2ETest : StudentTest() {
elementaryDashboardPage.selectTab(ElementaryDashboardPage.ElementaryTabType.IMPORTANT_DATES)
importantDatesPage.assertPageObjects()
- Log.d(STEP_TAG, "Assert that the important date assignments are displayed and the 'not' important (${testNotImportantAssignment.name}) is not displayed.")
+ Log.d(STEP_TAG, "Assert that the important date assignments are displayed and the 'not' important one, '${testNotImportantAssignment.name}' is not displayed.")
importantDatesPage.assertItemDisplayed(testAssignment1.name)
importantDatesPage.assertItemDisplayed(testAssignment2.name)
importantDatesPage.assertItemDisplayed(testAssignment3.name)
@@ -107,7 +106,7 @@ class ImportantDatesE2ETest : StudentTest() {
importantDatesPage.assertPageObjects()
Log.d(STEP_TAG, "Refresh the Important Dates page. Assert that the corresponding items" +
- "(all the assignments, except ${testNotImportantAssignment.name} named assignment) and their labels are still displayed after the refresh.")
+ "(all the assignments, except '${testNotImportantAssignment.name}' named assignment) and their labels are still displayed after the refresh.")
importantDatesPage.pullToRefresh()
importantDatesPage.assertItemDisplayed(testAssignment1.name)
importantDatesPage.assertItemDisplayed(testAssignment2.name)
@@ -118,32 +117,10 @@ class ImportantDatesE2ETest : StudentTest() {
importantDatesPage.assertRecyclerViewItemCount(3)
importantDatesPage.assertDayTextIsDisplayed(generateDayString(testAssignment1.dueAt.toDate()))
importantDatesPage.assertDayTextIsDisplayed(generateDayString(testAssignment2.dueAt.toDate()))
-
}
private fun generateDayString(date: Date?): String {
return SimpleDateFormat("EEEE, MMMM dd", Locale.getDefault()).format(date)
}
-
- private fun createAssignment(
- courseId: Long,
- teacher: CanvasUserApiModel,
- gradingType: GradingType,
- pointsPossible: Double,
- dueAt: String,
- importantDate: Boolean
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = courseId,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- gradingType = gradingType,
- teacherToken = teacher.token,
- pointsPossible = pointsPossible,
- dueAt = dueAt,
- importantDate = importantDate
- )
- )
- }
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ResourcesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ResourcesE2ETest.kt
index dfdf5c229d..0337f8d4c5 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ResourcesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ResourcesE2ETest.kt
@@ -21,6 +21,7 @@ import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
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.dataseeding.model.CanvasUserApiModel
@@ -33,13 +34,14 @@ import org.junit.Test
@HiltAndroidTest
class ResourcesE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@E2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.E2E, SecondaryFeatureCategory.RESOURCES)
fun resourcesE2ETest() {
Log.d(PREPARATION_TAG,"Seeding data for K5 sub-account.")
@@ -65,9 +67,9 @@ class ResourcesE2ETest : StudentTest() {
resourcesPage.assertPageObjects()
Log.d(STEP_TAG, "Assert that the important links, LTI tools and contacts are displayed.")
- assertElementaryResourcesPageInformations(teacher)
+ assertElementaryResourcesPageInformation(teacher)
- Log.d(STEP_TAG, "Click on the compose message icon next to a contact (${teacher.name}), and verify if the new message page is displayed.")
+ Log.d(STEP_TAG, "Click on the compose message icon next to a contact ('${teacher.name}' teacher), and verify if the new message page is displayed.")
resourcesPage.openComposeMessage(teacher.shortName)
assertNewMessagePageDisplayed()
@@ -76,7 +78,7 @@ class ResourcesE2ETest : StudentTest() {
resourcesPage.assertPageObjects()
Log.d(STEP_TAG, "Assert that the important links, LTI tools and contacts are still displayed correctly, after the navigation.")
- assertElementaryResourcesPageInformations(teacher)
+ assertElementaryResourcesPageInformation(teacher)
Log.d(STEP_TAG, "Open an LTI tool (Google Drive), and verify if all the NON-homeroom courses are displayed within the 'Choose a Course' list.")
resourcesPage.openLtiApp("Google Drive")
@@ -85,9 +87,7 @@ class ResourcesE2ETest : StudentTest() {
}
}
- private fun assertElementaryResourcesPageInformations(
- teacher: CanvasUserApiModel
- ) {
+ private fun assertElementaryResourcesPageInformation(teacher: CanvasUserApiModel) {
resourcesPage.assertImportantLinksHeaderDisplayed()
resourcesPage.assertStudentApplicationsHeaderDisplayed()
resourcesPage.assertStaffInfoHeaderDisplayed()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt
index a178f13783..edaa24c26d 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt
@@ -21,12 +21,11 @@ import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
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.canvasapi2.utils.toApiString
import com.instructure.dataseeding.api.AssignmentsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
import com.instructure.dataseeding.model.GradingType
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.espresso.page.getStringFromResource
@@ -40,6 +39,7 @@ import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Rule
import org.junit.Test
import org.junit.rules.Timeout
+import java.lang.Thread.sleep
import java.util.*
@HiltAndroidTest
@@ -50,11 +50,12 @@ class ScheduleE2ETest : StudentTest() {
override fun enableAndConfigureAccessibilityChecks() = Unit
@Rule
- var globalTimeout: Timeout = Timeout.millis(600000) // //TODO: workaround for that sometimes this test is running infinite time because of scrollToElement does not find an element.
+ @JvmField
+ var globalTimeout: Timeout = Timeout.millis(1200000) // //TODO: workaround for that sometimes this test is running infinite time because of scrollToElement does not find an element.
@E2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.E2E, SecondaryFeatureCategory.SCHEDULE)
fun scheduleE2ETest() {
Log.d(PREPARATION_TAG,"Seeding data for K5 sub-account.")
@@ -74,13 +75,13 @@ class ScheduleE2ETest : StudentTest() {
val twoWeeksAfterCalendar = getCustomDateCalendar(15)
Log.d(PREPARATION_TAG,"Seeding 'Text Entry' MISSING assignment for ${nonHomeroomCourses[2].name} course.")
- val testMissingAssignment = createAssignment(nonHomeroomCourses[2].id, teacher, currentDateCalendar, GradingType.LETTER_GRADE,100.0)
+ val testMissingAssignment = AssignmentsApi.createAssignment(nonHomeroomCourses[2].id, teacher.token, dueAt = currentDateCalendar.time.toApiString(), gradingType = GradingType.LETTER_GRADE, pointsPossible = 100.0, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
Log.d(PREPARATION_TAG,"Seeding 'Text Entry' Two weeks before end date assignment for ${nonHomeroomCourses[1].name} course.")
- val testTwoWeeksBeforeAssignment = createAssignment(nonHomeroomCourses[1].id, teacher, twoWeeksBeforeCalendar, GradingType.PERCENT,100.0)
+ val testTwoWeeksBeforeAssignment = AssignmentsApi.createAssignment(nonHomeroomCourses[1].id, teacher.token, dueAt = twoWeeksBeforeCalendar.time.toApiString(), gradingType = GradingType.PERCENT, pointsPossible = 100.0, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
Log.d(PREPARATION_TAG,"Seeding 'Text Entry' Two weeks after end date assignment for ${nonHomeroomCourses[0].name} course.")
- val testTwoWeeksAfterAssignment = createAssignment(nonHomeroomCourses[0].id, teacher, twoWeeksAfterCalendar, GradingType.POINTS,25.0)
+ val testTwoWeeksAfterAssignment = AssignmentsApi.createAssignment(nonHomeroomCourses[0].id, teacher.token, dueAt = twoWeeksAfterCalendar.time.toApiString(), gradingType = GradingType.POINTS, pointsPossible = 25.0, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
tokenLoginElementary(student)
@@ -88,7 +89,6 @@ class ScheduleE2ETest : StudentTest() {
Log.d(STEP_TAG, "Navigate to K5 Schedule Page and assert it is loaded.")
elementaryDashboardPage.selectTab(ElementaryDashboardPage.ElementaryTabType.SCHEDULE)
- schedulePage.assertPageObjects()
//Depends on how we handle Sunday, need to clarify with calendar team
if(currentDateCalendar.get(Calendar.DAY_OF_WEEK) != 1) { schedulePage.assertIfCourseHeaderAndScheduleItemDisplayed(homeroomCourse.name, homeroomAnnouncement.title) }
@@ -107,13 +107,13 @@ class ScheduleE2ETest : StudentTest() {
schedulePage.assertIfCourseHeaderAndScheduleItemDisplayed(nonHomeroomCourses[2].name, testMissingAssignment.name)
Log.d(STEP_TAG, "Scroll to 'Missing Items' section and verify that a missing assignment (${testMissingAssignment.name}) is displayed there with 100 points.")
- schedulePage.scrollToItem(R.id.missingItemLayout, testMissingAssignment.name)
- schedulePage.assertMissingItemDisplayed(testMissingAssignment.name, nonHomeroomCourses[2].name, "100 pts")
+ schedulePage.scrollToItem(R.id.metaLayout, testMissingAssignment.name)
+ schedulePage.assertMissingItemDisplayedOnPlannerItem(testMissingAssignment.name, nonHomeroomCourses[2].name, "100 pts")
Log.d(STEP_TAG, "Refresh the Schedule Page. Assert that the items are still displayed correctly.")
schedulePage.scrollToPosition(0)
schedulePage.refresh()
- schedulePage.assertPageObjects()
+ sleep(3000)
Log.d(STEP_TAG, "Assert that the current day of the calendar is titled as 'Today'.")
schedulePage.assertDayHeaderShownByItemName(concatDayString(currentDateCalendar), schedulePage.getStringFromResource(R.string.today), schedulePage.getStringFromResource(R.string.today))
@@ -158,6 +158,7 @@ class ScheduleE2ETest : StudentTest() {
Log.d(STEP_TAG, "Navigate back to Schedule Page and assert it is loaded.")
Espresso.pressBack()
+ sleep(3000)
schedulePage.assertPageObjects()
}
@@ -214,24 +215,5 @@ class ScheduleE2ETest : StudentTest() {
return cal
}
- private fun createAssignment(
- courseId: Long,
- teacher: CanvasUserApiModel,
- calendar: Calendar,
- gradingType: GradingType,
- pointsPossible: Double
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = courseId,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- gradingType = gradingType,
- teacherToken = teacher.token,
- pointsPossible = pointsPossible,
- dueAt = calendar.time.toApiString()
- )
- )
- }
-
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt
index 9aa20f8b6a..7f324a2cdc 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt
@@ -22,8 +22,10 @@ import com.google.android.material.checkbox.MaterialCheckBox
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.OfflineE2E
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.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
@@ -33,13 +35,14 @@ import org.junit.Test
@HiltAndroidTest
class ManageOfflineContentE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@OfflineE2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.OFFLINE_CONTENT, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.OFFLINE_CONTENT, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
fun testManageOfflineContentE2ETest() {
Log.d(PREPARATION_TAG,"Seeding data.")
@@ -48,7 +51,7 @@ class ManageOfflineContentE2ETest : StudentTest() {
val course1 = data.coursesList[0]
val course2 = data.coursesList[1]
- Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG,"Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
dashboardPage.waitForRender()
@@ -192,20 +195,13 @@ class ManageOfflineContentE2ETest : StudentTest() {
Log.d(STEP_TAG, "Click on the 'Sync' button and confirm sync.")
manageOfflineContentPage.clickOnSyncButtonAndConfirm()
- Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
- dashboardPage.waitForRender()
- dashboardPage.waitForSyncProgressDownloadStartedNotification()
- dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear()
-
- Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)")
- dashboardPage.waitForSyncProgressStartingNotification()
- dashboardPage.waitForSyncProgressStartingNotificationToDisappear()
+ Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced course's course card.")
+ dashboardPage.assertCourseOfflineSyncIconVisible(course2.name)
+ device.waitForIdle()
Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.")
turnOffConnectionViaADB()
- Thread.sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script.
- device.waitForIdle()
- device.waitForWindowUpdate(null, 10000)
+ waitForNetworkToGoOffline(device)
Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.")
dashboardPage.waitForRender()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt
index b45e9395bb..cb99b21f54 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt
@@ -19,8 +19,6 @@ package com.instructure.student.ui.e2e.offline
import android.util.Log
import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.UiDevice
import com.google.android.material.checkbox.MaterialCheckBox
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.OfflineE2E
@@ -28,16 +26,17 @@ 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.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Test
-import java.lang.Thread.sleep
@HiltAndroidTest
class OfflineAllCoursesE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -54,10 +53,7 @@ class OfflineAllCoursesE2ETest : StudentTest() {
val course2 = data.coursesList[1]
val course3 = data.coursesList[2]
- Log.d(PREPARATION_TAG, "Get the device to be able to perform app-independent actions on it.")
- val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-
- Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
dashboardPage.waitForRender()
@@ -83,23 +79,15 @@ class OfflineAllCoursesE2ETest : StudentTest() {
manageOfflineContentPage.changeItemSelectionState(course2.name)
manageOfflineContentPage.clickOnSyncButtonAndConfirm()
- Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
- dashboardPage.waitForRender()
- dashboardPage.waitForSyncProgressDownloadStartedNotification()
- dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear()
-
- Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)")
- dashboardPage.waitForSyncProgressStartingNotification()
- dashboardPage.waitForSyncProgressStartingNotificationToDisappear()
+ Log.d(STEP_TAG, "Assert that the offline sync icon is displayed on the synced (and favorited) course's course card.")
+ dashboardPage.assertCourseOfflineSyncIconVisible(course1.name)
+ device.waitForIdle()
Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.")
turnOffConnectionViaADB()
- sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script.
- device.waitForIdle()
- device.waitForWindowUpdate(null, 10000)
+ waitForNetworkToGoOffline(device)
Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered, and assert that '${course1.name}' is the only course which is displayed on the offline mode Dashboard Page.")
- dashboardPage.waitForRender()
dashboardPage.assertDisplaysCourse(course1)
dashboardPage.assertCourseNotDisplayed(course2)
dashboardPage.assertCourseNotDisplayed(course3)
@@ -136,7 +124,6 @@ class OfflineAllCoursesE2ETest : StudentTest() {
Log.d(STEP_TAG, "Click on '${course1.name}' course and assert if it will navigate the user to the CourseBrowser Page.")
allCoursesPage.openCourse(course1.name)
courseBrowserPage.assertTitleCorrect(course1)
-
}
@After
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAnnouncementsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAnnouncementsE2ETest.kt
new file mode 100644
index 0000000000..77c31f5877
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAnnouncementsE2ETest.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2024 - 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.offline
+
+import android.util.Log
+import androidx.test.espresso.Espresso
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.OfflineE2E
+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.dataseeding.api.DiscussionTopicsApi
+import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils
+import com.instructure.student.ui.utils.StudentTest
+import com.instructure.student.ui.utils.seedData
+import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.After
+import org.junit.Test
+import java.lang.Thread.sleep
+
+@HiltAndroidTest
+class OfflineAnnouncementsE2ETest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() = Unit
+
+ @OfflineE2E
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
+ fun testOfflineAnnouncementsE2E() {
+
+ Log.d(PREPARATION_TAG,"Seeding data.")
+ val data = seedData(students = 1, teachers = 1, courses = 1, announcements = 1)
+ val student = data.studentsList[0]
+ val teacher = data.teachersList[0]
+ val course = data.coursesList[0]
+ val announcement = data.announcementsList[0]
+
+ val lockedAnnouncement = DiscussionTopicsApi.createAnnouncement(course.id, teacher.token, locked = true)
+
+ 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 the 'Announcements' of '${course.name}' course for sync. Click on the 'Sync' button.")
+ manageOfflineContentPage.changeItemSelectionState("Announcements")
+ manageOfflineContentPage.clickOnSyncButtonAndConfirm()
+
+ Log.d(STEP_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()
+
+ Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.")
+ OfflineTestUtils.assertOfflineIndicator()
+
+ Log.d(STEP_TAG, "Select '${course.name}' course and open 'Announcements' menu.")
+ dashboardPage.selectCourse(course)
+ courseBrowserPage.selectAnnouncements()
+
+ Log.d(STEP_TAG,"Assert that '${announcement.title}' announcement is displayed.")
+ announcementListPage.assertTopicDisplayed(announcement.title)
+
+ Log.d(STEP_TAG, "Assert that '${lockedAnnouncement.title}' announcement is really locked so that the 'locked' icon is displayed.")
+ announcementListPage.assertAnnouncementLocked(lockedAnnouncement.title)
+
+ Log.d(STEP_TAG, "Select '${lockedAnnouncement.title}' announcement and assert if we are landing on the Discussion Details Page.")
+ announcementListPage.selectTopic(lockedAnnouncement.title)
+ discussionDetailsPage.assertTitleText(lockedAnnouncement.title)
+
+ Log.d(STEP_TAG, "Assert that the 'Reply' button is not available on a locked announcement. Navigate back to Announcement List Page.")
+ discussionDetailsPage.assertReplyButtonNotDisplayed()
+ Espresso.pressBack()
+
+ Log.d(STEP_TAG,"Select '${announcement.title}' announcement and assert if we are landing on the Discussion Details Page.")
+ announcementListPage.selectTopic(announcement.title)
+ discussionDetailsPage.assertTitleText(announcement.title)
+
+ Log.d(STEP_TAG, "Click on the 'Reply' button and assert that the 'No Internet Connection' dialog has displayed. Dismiss the dialog.")
+ discussionDetailsPage.clickReply()
+ OfflineTestUtils.assertNoInternetConnectionDialog()
+ OfflineTestUtils.dismissNoInternetConnectionDialog()
+
+ Log.d(STEP_TAG,"Navigate back to Announcement List page. Click on Search button and type '${announcement.title}' to the search input field.")
+ Espresso.pressBack()
+ announcementListPage.searchable.clickOnSearchButton()
+ announcementListPage.searchable.typeToSearchBar(announcement.title)
+
+ Log.d(STEP_TAG,"Assert that only the matching announcement is displayed on the Discussion List Page.")
+ announcementListPage.pullToUpdate()
+ announcementListPage.assertTopicDisplayed(announcement.title)
+ announcementListPage.assertTopicNotDisplayed(lockedAnnouncement.title)
+
+ Log.d(STEP_TAG,"Clear search input field value and assert if all the announcements are displayed again on the Discussion List Page.")
+ announcementListPage.searchable.clickOnClearSearchButton()
+ announcementListPage.waitForDiscussionTopicToDisplay(lockedAnnouncement.title)
+ announcementListPage.assertTopicDisplayed(announcement.title)
+
+ Log.d(STEP_TAG,"Type the '${announcement.title}' announcement's title as search value to the search input field. Assert the the '${announcement.title}' announcement, but only that displayed as result.")
+ announcementListPage.searchable.typeToSearchBar(announcement.title)
+ sleep(3000) //We need this wait here to let make sure the search process has finished.
+ announcementListPage.assertTopicDisplayed(announcement.title)
+ announcementListPage.assertTopicNotDisplayed(lockedAnnouncement.title)
+
+ Log.d(STEP_TAG,"Clear search input field value and assert if both the announcements are displayed again on the Announcement List Page.")
+ announcementListPage.searchable.clickOnClearSearchButton()
+ announcementListPage.waitForDiscussionTopicToDisplay(lockedAnnouncement.title)
+ announcementListPage.assertTopicDisplayed(announcement.title)
+ }
+
+ @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.")
+ turnOnConnectionViaADB()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt
index 5ea2aeb906..017eed51df 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt
@@ -18,13 +18,13 @@ package com.instructure.student.ui.e2e.offline
import android.util.Log
import androidx.test.espresso.Espresso
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.UiDevice
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.OfflineE2E
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.student.ui.e2e.offline.utils.OfflineTestUtils
import com.instructure.student.ui.pages.CourseBrowserPage
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
@@ -32,59 +32,49 @@ import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Test
-import java.lang.Thread.sleep
@HiltAndroidTest
class OfflineCourseBrowserE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@OfflineE2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.COURSE, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.COURSE, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
fun testOfflineCourseBrowserPageUnavailableE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
val data = seedData(students = 1, teachers = 1, courses = 1, announcements = 1)
val student = data.studentsList[0]
- val course1 = data.coursesList[0]
- val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ val course = data.coursesList[0]
- Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG,"Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
dashboardPage.waitForRender()
Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.")
dashboardPage.openGlobalManageOfflineContentPage()
- Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.")
- Log.d(STEP_TAG, "Expand '${course1.name}' course. Select only the 'Announcements' of the '${course1.name}' course. Click on the 'Sync' button and confirm the sync process.")
- manageOfflineContentPage.expandCollapseItem(course1.name)
+ Log.d(STEP_TAG, "Expand '${course.name}' course. Select only the 'Announcements' of the '${course.name}' course. Click on the 'Sync' button and confirm the sync process.")
+ manageOfflineContentPage.expandCollapseItem(course.name)
manageOfflineContentPage.changeItemSelectionState("Announcements")
manageOfflineContentPage.clickOnSyncButtonAndConfirm()
- Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
- dashboardPage.waitForRender()
- dashboardPage.waitForSyncProgressDownloadStartedNotification()
- dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear()
-
- Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)")
- dashboardPage.waitForSyncProgressStartingNotification()
- dashboardPage.waitForSyncProgressStartingNotificationToDisappear()
+ Log.d(STEP_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()
- sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script.
- device.waitForIdle()
- device.waitForWindowUpdate(null, 10000)
+ OfflineTestUtils.waitForNetworkToGoOffline(device)
Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.")
dashboardPage.waitForRender()
- Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.")
- sleep(5000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script.
- dashboardPage.selectCourse(course1)
+ Log.d(STEP_TAG, "Select '${course.name}' course and open 'Announcements' menu.")
+ dashboardPage.selectCourse(course)
Log.d(STEP_TAG, "Assert that only the 'Announcements' tab is enabled because it is the only one which has been synced, and assert that all the other, previously synced tabs are disabled, because they weren't synced now.")
var enabledTabs = arrayOf("Announcements")
@@ -95,32 +85,27 @@ class OfflineCourseBrowserE2ETest : StudentTest() {
Log.d(STEP_TAG, "Navigate back to Dashboard Page.Turn back on the Wi-Fi and Mobile Data on the device, and wait for it to come online.")
Espresso.pressBack()
turnOnConnectionViaADB()
- dashboardPage.waitForNetworkComeBack()
+ dashboardPage.waitForOfflineIndicatorNotDisplayed()
Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.")
dashboardPage.openGlobalManageOfflineContentPage()
- Log.d(STEP_TAG, "Deselect the entire '${course1.name}' course for sync.")
- manageOfflineContentPage.changeItemSelectionState(course1.name)
+ Log.d(STEP_TAG, "Deselect the entire '${course.name}' course for sync.")
+ manageOfflineContentPage.changeItemSelectionState(course.name)
manageOfflineContentPage.clickOnSyncButtonAndConfirm()
- Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
- dashboardPage.waitForRender()
- dashboardPage.waitForSyncProgressDownloadStartedNotification()
- dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear()
-
- Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)")
- dashboardPage.waitForSyncProgressStartingNotification()
- dashboardPage.waitForSyncProgressStartingNotificationToDisappear()
+ Log.d(STEP_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()
- Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.")
- sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script.
- device.waitForIdle()
- device.waitForWindowUpdate(null, 10000)
- dashboardPage.selectCourse(course1)
+ Log.d(STEP_TAG, "Select '${course.name}' course and open 'Announcements' menu.")
+ OfflineTestUtils.waitForNetworkToGoOffline(device)
+
+ Log.d(STEP_TAG, "Select '${course.name}' course.")
+ dashboardPage.selectCourse(course)
Log.d(STEP_TAG, "Assert that the 'Google Drive' and 'Collaborations' tabs are disabled because they aren't supported in offline mode, but the rest of the tabs are enabled because the whole course has been synced.")
enabledTabs = arrayOf("Announcements", "Discussions", "Grades", "People", "Syllabus", "BigBlueButton")
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt
index 4be5df3180..c49034b82d 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt
@@ -27,6 +27,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory
import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils
+import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
@@ -36,13 +37,14 @@ import org.junit.Test
@HiltAndroidTest
class OfflineDashboardE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@OfflineE2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
fun testOfflineDashboardE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
@@ -52,10 +54,7 @@ class OfflineDashboardE2ETest : StudentTest() {
val course2 = data.coursesList[1]
val testAnnouncement = data.announcementsList[0]
- Log.d(PREPARATION_TAG, "Get the device to be able to perform app-independent actions on it.")
- val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-
- Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG,"Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
dashboardPage.waitForRender()
@@ -69,20 +68,13 @@ class OfflineDashboardE2ETest : StudentTest() {
manageOfflineContentPage.changeItemSelectionState(course1.name)
manageOfflineContentPage.clickOnSyncButtonAndConfirm()
- Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
- dashboardPage.waitForRender()
- dashboardPage.waitForSyncProgressDownloadStartedNotification()
- dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear()
-
- Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)")
- dashboardPage.waitForSyncProgressStartingNotification()
- dashboardPage.waitForSyncProgressStartingNotificationToDisappear()
+ Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced course's course card.")
+ dashboardPage.assertCourseOfflineSyncIconVisible(course1.name)
+ device.waitForIdle()
Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.")
turnOffConnectionViaADB()
- Thread.sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script.
- device.waitForIdle()
- device.waitForWindowUpdate(null, 10000)
+ waitForNetworkToGoOffline(device)
Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.")
dashboardPage.waitForRender()
@@ -126,18 +118,13 @@ class OfflineDashboardE2ETest : StudentTest() {
manageOfflineContentPage.changeItemSelectionState(course.name)
manageOfflineContentPage.clickOnSyncButtonAndConfirm()
- Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
- dashboardPage.waitForRender()
- dashboardPage.waitForSyncProgressDownloadStartedNotification()
- dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear()
-
- Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)")
- dashboardPage.waitForSyncProgressStartingNotification()
- dashboardPage.waitForSyncProgressStartingNotificationToDisappear()
+ Log.d(STEP_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()
- device.waitForIdle()
+ waitForNetworkToGoOffline(device)
Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.")
dashboardPage.waitForRender()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt
new file mode 100644
index 0000000000..2fccbc67e4
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2024 - 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.offline
+
+import android.util.Log
+import androidx.test.espresso.Espresso
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.OfflineE2E
+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.dataseeding.api.DiscussionTopicsApi
+import com.instructure.espresso.getCurrentDateInCanvasFormat
+import com.instructure.student.R
+import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils
+import com.instructure.student.ui.utils.StudentTest
+import com.instructure.student.ui.utils.ViewUtils
+import com.instructure.student.ui.utils.openOverflowMenu
+import com.instructure.student.ui.utils.seedData
+import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.After
+import org.junit.Test
+import java.lang.Thread.sleep
+
+@HiltAndroidTest
+class OfflineDiscussionsE2ETest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() = Unit
+
+ @OfflineE2E
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
+ fun testOfflineDiscussionsE2E() {
+
+ Log.d(PREPARATION_TAG,"Seeding data.")
+ val data = seedData(students = 1, teachers = 1, courses = 1)
+ val student = data.studentsList[0]
+ val teacher = data.teachersList[0]
+ val course = data.coursesList[0]
+
+ Log.d(PREPARATION_TAG,"Seed a discussion topic for '${course.name}' course.")
+ val discussion1 = DiscussionTopicsApi.createDiscussion(course.id, teacher.token)
+
+ Log.d(PREPARATION_TAG,"Seed another discussion topic for '${course.name}' course.")
+ val discussion2 = DiscussionTopicsApi.createDiscussion(course.id, teacher.token)
+
+ 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, "Select '${course.name}' course and navigate to Discussion List page.")
+ dashboardPage.selectCourse(course)
+ courseBrowserPage.selectDiscussions()
+
+ Log.d(STEP_TAG,"Select '$discussion1' discussion topic and assert that there is no reply on the details page as well.")
+ discussionListPage.selectTopic(discussion1.title)
+ discussionDetailsPage.assertNoRepliesDisplayed()
+
+ val replyMessage = "My reply"
+ Log.d(STEP_TAG,"Send a reply with text: '$replyMessage'.")
+ discussionDetailsPage.sendReply(replyMessage)
+ sleep(2000) // Allow some time for reply to propagate
+
+ Log.d(STEP_TAG,"Assert the the previously sent reply '$replyMessage', is displayed on the details page.")
+ discussionDetailsPage.assertRepliesDisplayed()
+
+ Log.d(STEP_TAG, "Navigate back to the Dashboard page.")
+ ViewUtils.pressBackButton(3)
+
+ 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 the 'Discussions' of '${course.name}' course for sync. Click on the 'Sync' button.")
+ manageOfflineContentPage.changeItemSelectionState("Discussions")
+ manageOfflineContentPage.clickOnSyncButtonAndConfirm()
+
+ Log.d(STEP_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()
+
+ Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.")
+ OfflineTestUtils.assertOfflineIndicator()
+
+ Log.d(STEP_TAG, "Select '${course.name}' course and open 'Announcements' menu.")
+ dashboardPage.selectCourse(course)
+
+ Log.d(STEP_TAG,"Navigate to Discussion List Page.")
+ courseBrowserPage.selectDiscussions()
+
+ Log.d(STEP_TAG, "Assert that both the '${discussion1.title}' and '${discussion2.title}' discussion are displayed on the Discussion List page.")
+ discussionListPage.assertTopicDisplayed(discussion1.title)
+ discussionListPage.assertTopicDisplayed(discussion2.title)
+
+ Log.d(STEP_TAG,"Refresh the page. Assert that the previously sent reply has been counted, and there are no unread replies.")
+ discussionListPage.assertReplyCount(discussion1.title, 1)
+ discussionListPage.assertUnreadReplyCount(discussion1.title, 0)
+
+ Log.d(STEP_TAG, "Assert that the due date is the current date (in the expected format).")
+ val currentDate = getCurrentDateInCanvasFormat()
+ discussionListPage.assertDueDate(discussion1.title, currentDate)
+
+ Log.d(STEP_TAG, "Click on the Search (magnifying glass) icon and the '${discussion1.title}' discussion's title into the search input field.")
+ discussionListPage.searchable.clickOnSearchButton()
+ discussionListPage.searchable.typeToSearchBar(discussion1.title)
+
+ Log.d(STEP_TAG, "Assert that only the '${discussion1.title}' discussion displayed as a search result and the other, '${discussion2.title}' discussion has not displayed.")
+ discussionListPage.assertTopicDisplayed(discussion1.title)
+ discussionListPage.assertTopicNotDisplayed(discussion2.title)
+
+ Log.d(STEP_TAG, "Click on the 'Clear Search' (X) icon and assert that both of the discussion should be displayed again.")
+ discussionListPage.searchable.clickOnClearSearchButton()
+ discussionListPage.waitForDiscussionTopicToDisplay(discussion2.title)
+ discussionListPage.assertTopicDisplayed(discussion1.title)
+
+ Log.d(STEP_TAG,"Select '${discussion1.title}' discussion and assert if the corresponding discussion title is displayed.")
+ discussionListPage.selectTopic(discussion1.title)
+ discussionDetailsPage.assertTitleText(discussion1.title)
+
+ Log.d(STEP_TAG, "Try to click on the (main) 'Reply' button and assert that the 'No Internet Connection' dialog has displayed. Dismiss the dialog.")
+ discussionDetailsPage.clickReply()
+ OfflineTestUtils.assertNoInternetConnectionDialog()
+ OfflineTestUtils.dismissNoInternetConnectionDialog()
+
+ Log.d(STEP_TAG, "Try to click on the (inner) 'Reply' button (so try to 'reply to a reply') and assert that the 'No Internet Connection' dialog has displayed. Dismiss the dialog.")
+ discussionDetailsPage.clickOnInnerReply()
+ OfflineTestUtils.assertNoInternetConnectionDialog()
+ OfflineTestUtils.dismissNoInternetConnectionDialog()
+
+ Log.d(STEP_TAG,"Navigate back to Discussion List Page.")
+ Espresso.pressBack()
+
+ Log.d(STEP_TAG,"Select '${discussion2.title}' discussion and assert if the Discussion Details page is displayed and there is no reply for the discussion yet.")
+ discussionListPage.selectTopic(discussion2.title)
+ discussionDetailsPage.assertTitleText(discussion2.title)
+ discussionDetailsPage.assertNoRepliesDisplayed()
+
+ Log.d(STEP_TAG, "Try to click on 'Add Bookmark' overflow menu and assert that the 'Functionality unavailable while offline' toast message is displayed.")
+ openOverflowMenu()
+ discussionDetailsPage.clickOnAddBookmarkMenu()
+ checkToastText(R.string.notAvailableOffline, activityRule.activity)
+ }
+
+ @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.")
+ turnOnConnectionViaADB()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineFilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineFilesE2ETest.kt
index a96cba7482..1670437cc1 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineFilesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineFilesE2ETest.kt
@@ -22,10 +22,12 @@ import com.google.android.material.checkbox.MaterialCheckBox
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.OfflineE2E
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.dataseeding.api.FileFolderApi
import com.instructure.dataseeding.model.FileUploadType
+import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
@@ -36,13 +38,14 @@ import org.junit.Test
@HiltAndroidTest
class OfflineFilesE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@OfflineE2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.FILES, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.FILES, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
fun testOfflineFilesE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
@@ -54,7 +57,7 @@ class OfflineFilesE2ETest : StudentTest() {
val testCourseFolderName = "Goodya"
Log.d(PREPARATION_TAG, "Create a course folder within the 'Files' tab with the name: '$testCourseFolderName'.")
val courseRootFolder = FileFolderApi.getCourseRootFolder(course.id, teacher.token)
- val courseTestFolder = FileFolderApi.createCourseFolder(courseRootFolder.id, testCourseFolderName, false, teacher.token)
+ val courseTestFolder = FileFolderApi.createCourseFolder(courseRootFolder.id, teacher.token, testCourseFolderName)
Log.d(PREPARATION_TAG, "Create a (text) file within the root folder (so the 'Files' tab file list) of the '${course.name}' course.")
val rootFolderTestTextFile = uploadTextFile(courseRootFolder.id, token = teacher.token, fileUploadType = FileUploadType.COURSE_FILE)
@@ -62,7 +65,7 @@ class OfflineFilesE2ETest : StudentTest() {
Log.d(PREPARATION_TAG, "Create a (text) file within the '${courseTestFolder.name}' folder of the '${course.name}' course.")
val courseTestFolderTextFile = uploadTextFile(courseTestFolder.id, token = teacher.token, fileUploadType = FileUploadType.COURSE_FILE)
- Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG,"Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
dashboardPage.waitForRender()
@@ -77,20 +80,13 @@ class OfflineFilesE2ETest : StudentTest() {
manageOfflineContentPage.changeItemSelectionState("Files")
manageOfflineContentPage.clickOnSyncButtonAndConfirm()
- Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
- dashboardPage.waitForRender()
- dashboardPage.waitForSyncProgressDownloadStartedNotification()
- dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear()
-
- Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)")
- dashboardPage.waitForSyncProgressStartingNotification()
- dashboardPage.waitForSyncProgressStartingNotificationToDisappear()
+ Log.d(STEP_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()
- Thread.sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script.
- device.waitForIdle()
- device.waitForWindowUpdate(null, 10000)
+ OfflineTestUtils.waitForNetworkToGoOffline(device)
Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.")
dashboardPage.waitForRender()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt
index 3b185a5d14..84bcbdd3be 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt
@@ -20,8 +20,10 @@ import android.util.Log
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.OfflineE2E
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.student.ui.e2e.offline.utils.OfflineTestUtils
import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertNoInternetConnectionDialog
import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator
import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.dismissNoInternetConnectionDialog
@@ -34,33 +36,35 @@ import org.junit.Test
@HiltAndroidTest
class OfflineLeftSideMenuE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@OfflineE2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
fun testOfflineLeftSideMenuUnavailableFunctionsE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1)
+ val data = seedData(students = 1, teachers = 1, courses = 1, announcements = 1)
val student = data.studentsList[0]
- Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG,"Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
dashboardPage.waitForRender()
Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.")
turnOffConnectionViaADB()
- Thread.sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script.
+ OfflineTestUtils.waitForNetworkToGoOffline(device)
+ Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.")
dashboardPage.waitForRender()
Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.")
assertOfflineIndicator()
- Log.d(STEP_TAG, "Open Left Side Menu by clicking on the 'hamburger icon' on the Dashboard Page.")
+ Log.d(STEP_TAG, "Open Left Side Menu by clicking on the 'hamburger/kebab icon' on the Dashboard Page.")
dashboardPage.openLeftSideMenu()
Log.d(STEP_TAG, "Assert that the offline indicator is displayed below the user info within the header.")
@@ -91,7 +95,6 @@ class OfflineLeftSideMenuE2ETest : StudentTest() {
leftSideNavigationDrawerPage.clickHelpMenu()
assertNoInternetConnectionDialog()
dismissNoInternetConnectionDialog()
-
}
@After
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt
index a4718ab009..810839655b 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt
@@ -26,6 +26,7 @@ import com.instructure.canvas.espresso.TestMetaData
import com.instructure.dataseeding.model.CanvasUserApiModel
import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertNoInternetConnectionDialog
import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.dismissNoInternetConnectionDialog
+import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import dagger.hilt.android.testing.HiltAndroidTest
@@ -35,13 +36,14 @@ import java.lang.Thread.sleep
@HiltAndroidTest
class OfflineLoginE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@OfflineE2E
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.LOGIN, TestCategory.E2E, SecondaryFeatureCategory.CHANGE_USER)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.LOGIN, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
fun testOfflineChangeUserE2E() {
Log.d(PREPARATION_TAG, "Seeding data.")
@@ -49,7 +51,7 @@ class OfflineLoginE2ETest : StudentTest() {
val student1 = data.studentsList[0]
val student2 = data.studentsList[1]
- Log.d(STEP_TAG, "Login with user: ${student1.name}, login id: ${student1.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${student1.name}', login id: '${student1.loginId}'.")
loginWithUser(student1)
dashboardPage.waitForRender()
@@ -59,8 +61,10 @@ class OfflineLoginE2ETest : StudentTest() {
Log.d(STEP_TAG, "Click on 'Change User' button on the left-side menu.")
leftSideNavigationDrawerPage.clickChangeUserMenu()
- Log.d(STEP_TAG, "Login with user: ${student2.name}, login id: ${student2.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${student2.name}', login id: '${student2.loginId}'.")
loginWithUser(student2, true)
+
+ Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.")
dashboardPage.waitForRender()
Log.d(STEP_TAG, "Assert that the Offline indicator is not displayed because we are in online mode yet.")
@@ -68,12 +72,12 @@ class OfflineLoginE2ETest : StudentTest() {
Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.")
turnOffConnectionViaADB()
- device.waitForWindowUpdate(null, 10000)
+ waitForNetworkToGoOffline(device)
Log.d(STEP_TAG, "Click on 'Change User' button on the left-side menu.")
leftSideNavigationDrawerPage.clickChangeUserMenu()
- Log.d(STEP_TAG, "Assert that the previously logins has been displayed. Assert that ${student1.name} and ${student2.name} students are displayed within the previous login section.")
+ Log.d(STEP_TAG, "Assert that the previously logins has been displayed. Assert that '${student1.name}' and '${student2.name}' students are displayed within the previous login section.")
loginLandingPage.assertDisplaysPreviousLogins()
loginLandingPage.assertPreviousLoginUserDisplayed(student1.name)
loginLandingPage.assertPreviousLoginUserDisplayed(student2.name)
@@ -88,7 +92,7 @@ class OfflineLoginE2ETest : StudentTest() {
assertNoInternetConnectionDialog()
dismissNoInternetConnectionDialog()
- Log.d(STEP_TAG, "Login with the previous user, ${student1.name}, with one click, by clicking on the user's name on the bottom.")
+ Log.d(STEP_TAG, "Login with the previous user, '${student1.name}', with one click, by clicking on the user's name on the bottom.")
loginLandingPage.loginWithPreviousUser(student1)
Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Assert that the offline indicator is displayed to ensure we are in offline mode, and change user function is supported.")
@@ -98,13 +102,12 @@ class OfflineLoginE2ETest : StudentTest() {
Log.d(STEP_TAG, "Click on 'Change User' button on the left-side menu.")
leftSideNavigationDrawerPage.clickChangeUserMenu()
- Log.d(STEP_TAG, "Login with the previous user, ${student2.name}, with one click, by clicking on the user's name on the bottom.")
+ Log.d(STEP_TAG, "Login with the previous user, '${student2.name}', with one click, by clicking on the user's name on the bottom.")
loginLandingPage.loginWithPreviousUser(student2)
Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Assert that the offline indicator is displayed to ensure we are in offline mode, and change user function is supported.")
dashboardPage.waitForRender()
dashboardPage.assertOfflineIndicatorDisplayed()
-
}
private fun loginWithUser(user: CanvasUserApiModel, lastSchoolSaved: Boolean = false) {
@@ -120,7 +123,7 @@ class OfflineLoginE2ETest : StudentTest() {
loginLandingPage.clickFindMySchoolButton()
}
- Log.d(STEP_TAG,"Enter domain: ${user.domain}.")
+ Log.d(STEP_TAG,"Enter domain: '${user.domain}'.")
loginFindSchoolPage.enterDomain(user.domain)
Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.")
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePagesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePagesE2ETest.kt
new file mode 100644
index 0000000000..c2a949ce28
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePagesE2ETest.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 - 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.offline
+
+import android.util.Log
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.web.webdriver.Locator
+import com.google.android.material.checkbox.MaterialCheckBox
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.OfflineE2E
+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.dataseeding.api.PagesApi
+import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils
+import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator
+import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline
+import com.instructure.student.ui.pages.WebViewTextCheck
+import com.instructure.student.ui.utils.StudentTest
+import com.instructure.student.ui.utils.seedData
+import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.After
+import org.junit.Test
+
+@HiltAndroidTest
+class OfflinePagesE2ETest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() = Unit
+
+ @OfflineE2E
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.PAGES, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
+ fun testOfflinePagesE2E() {
+
+ Log.d(PREPARATION_TAG,"Seeding data.")
+ val data = seedData(students = 1, teachers = 1, courses = 1)
+ val student = data.studentsList[0]
+ val teacher = data.teachersList[0]
+ val course = data.coursesList[0]
+
+ Log.d(PREPARATION_TAG,"Seed an UNPUBLISHED page for '${course.name}' course.")
+ val pageUnpublished = PagesApi.createCoursePage(course.id, teacher.token, published = false)
+
+ Log.d(PREPARATION_TAG,"Seed a PUBLISHED page for '${course.name}' course.")
+ val pagePublished = PagesApi.createCoursePage(course.id, teacher.token, editingRoles = "teachers,students", body = "")
+
+ Log.d(PREPARATION_TAG,"Seed a PUBLISHED, but NOT editable page for '${course.name}' course.")
+ val pageNotEditable = PagesApi.createCoursePage(course.id, teacher.token, body = "")
+
+ Log.d(PREPARATION_TAG,"Seed a PUBLISHED, FRONT page for '${course.name}' course.")
+ val pagePublishedFront = PagesApi.createCoursePage(course.id, teacher.token, frontPage = true, editingRoles = "public", body = "")
+
+ 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, "Assert that the '${course.name}' course's checkbox state is 'Unchecked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course.name, MaterialCheckBox.STATE_UNCHECKED)
+
+ Log.d(STEP_TAG, "Expand the course. Select the 'Pages' of '${course.name}' course for sync. Click on the 'Sync' button.")
+ manageOfflineContentPage.expandCollapseItem(course.name)
+ manageOfflineContentPage.changeItemSelectionState("Pages")
+ manageOfflineContentPage.clickOnSyncButtonAndConfirm()
+
+ Log.d(STEP_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()
+ waitForNetworkToGoOffline(device)
+
+ Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.")
+ dashboardPage.waitForRender()
+
+ Log.d(STEP_TAG, "Select '${course.name}' course and click on 'Pages' tab to navigate to the Page List Page.")
+ dashboardPage.selectCourse(course)
+ courseBrowserPage.selectPages()
+
+ Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Page List Page.")
+ assertOfflineIndicator()
+
+ Log.d(STEP_TAG,"Assert that '${pagePublishedFront.title}' published front page is displayed.")
+ pageListPage.assertFrontPageDisplayed(pagePublishedFront)
+
+ Log.d(STEP_TAG,"Assert that '${pagePublished.title}' published page is displayed.")
+ pageListPage.assertRegularPageDisplayed(pagePublished)
+
+ Log.d(STEP_TAG,"Assert that '${pageUnpublished.title}' unpublished page is NOT displayed.")
+ pageListPage.assertPageNotDisplayed(pageUnpublished)
+
+ Log.d(STEP_TAG, "Click on 'Search' (magnifying glass) icon and type '${pagePublishedFront.title}', the page's name to the search input field.")
+ pageListPage.searchable.clickOnSearchButton()
+ pageListPage.searchable.typeToSearchBar(pagePublishedFront.title)
+
+ Log.d(STEP_TAG,"Assert that '${pagePublished.title}' published page is NOT displayed and there is only one page (the front page) is displayed.")
+ pageListPage.assertPageNotDisplayed(pagePublished)
+ pageListPage.assertPageListItemCount(1)
+
+ Log.d(STEP_TAG, "Click on clear search icon (X).")
+ pageListPage.searchable.clickOnClearSearchButton()
+
+ Log.d(STEP_TAG,"Assert that '${pagePublishedFront.title}' published front page is displayed.")
+ pageListPage.assertFrontPageDisplayed(pagePublishedFront)
+
+ Log.d(STEP_TAG,"Assert that '${pagePublished.title}' published page is displayed.")
+ pageListPage.assertRegularPageDisplayed(pagePublished)
+
+ Log.d(STEP_TAG,"Assert that '${pageUnpublished.title}' unpublished page is NOT displayed.")
+ pageListPage.assertPageNotDisplayed(pageUnpublished)
+
+ Log.d(STEP_TAG,"Open '${pagePublishedFront.title}' page. Assert that it is really a front (published) page via web view assertions.")
+ pageListPage.selectFrontPage(pagePublishedFront)
+ canvasWebViewPage.runTextChecks(WebViewTextCheck(Locator.ID, "header1", "Front Page Text"))
+
+ Log.d(STEP_TAG,"Navigate back to Pages page.")
+ Espresso.pressBack()
+
+ Log.d(STEP_TAG, "Select '${pageNotEditable.title}' page. Assert that it is not editable as a student, then navigate back to Page List page.")
+ pageListPage.selectRegularPage(pageNotEditable)
+ canvasWebViewPage.assertDoesNotEditable()
+ Espresso.pressBack()
+
+ Log.d(STEP_TAG,"Open '${pagePublished.title}' page. Assert that it is really a regular published page via web view assertions.")
+ pageListPage.selectRegularPage(pagePublished)
+ canvasWebViewPage.runTextChecks(WebViewTextCheck(Locator.ID, "header1", "Regular Page Text"))
+
+ Log.d(STEP_TAG, "Click on the 'Pencil' icon. Assert that the 'No Internet Connection' dialog has displayed. Dismiss the dialog by accepting it.")
+ canvasWebViewPage.clickEditPencilIcon()
+ OfflineTestUtils.assertNoInternetConnectionDialog()
+ OfflineTestUtils.dismissNoInternetConnectionDialog()
+
+ Log.d(STEP_TAG, "Navigate back to Page List page. Select '${pagePublishedFront.title}' front page.")
+ Espresso.pressBack()
+ pageListPage.selectFrontPage(pagePublishedFront)
+
+ Log.d(STEP_TAG, "Click on the 'Pencil' icon. Assert that the 'No Internet Connection' dialog has displayed. Dismiss the dialog by accepting it.")
+ canvasWebViewPage.clickEditPencilIcon()
+ OfflineTestUtils.assertNoInternetConnectionDialog()
+ OfflineTestUtils.dismissNoInternetConnectionDialog()
+ }
+
+ @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.")
+ turnOnConnectionViaADB()
+ }
+
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePeopleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePeopleE2ETest.kt
index 707757c566..a48ff20b91 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePeopleE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePeopleE2ETest.kt
@@ -18,11 +18,10 @@ package com.instructure.student.ui.e2e.offline
import android.util.Log
import androidx.test.espresso.matcher.ViewMatchers
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.UiDevice
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.OfflineE2E
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.student.ui.e2e.offline.utils.OfflineTestUtils
@@ -35,13 +34,14 @@ import org.junit.Test
@HiltAndroidTest
class OfflinePeopleE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@OfflineE2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.PEOPLE, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.PEOPLE, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
fun testOfflinePeopleE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
@@ -50,35 +50,27 @@ class OfflinePeopleE2ETest : StudentTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG, "Get the device to be able to perform app-independent actions on it.")
- val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-
- Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
+ 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 the entire '${course.name}' course for sync. Click on the 'Sync' button.")
+
+ Log.d(STEP_TAG, "Select the 'People' of '${course.name}' course for sync. Click on the 'Sync' button.")
manageOfflineContentPage.changeItemSelectionState("People")
manageOfflineContentPage.clickOnSyncButtonAndConfirm()
- Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
- dashboardPage.waitForRender()
- dashboardPage.waitForSyncProgressDownloadStartedNotification()
- dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear()
-
- Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)")
- dashboardPage.waitForSyncProgressStartingNotification()
- dashboardPage.waitForSyncProgressStartingNotificationToDisappear()
+ Log.d(STEP_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()
- Thread.sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script.
- device.waitForIdle()
- device.waitForWindowUpdate(null, 10000)
+ OfflineTestUtils.waitForNetworkToGoOffline(device)
Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.")
dashboardPage.waitForRender()
@@ -86,9 +78,6 @@ class OfflinePeopleE2ETest : StudentTest() {
Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.")
OfflineTestUtils.assertOfflineIndicator()
- Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced course's course card.")
- dashboardPage.assertCourseOfflineSyncIconVisible(course.name)
-
Log.d(STEP_TAG, "Select '${course.name}' course and open 'People' menu.")
dashboardPage.selectCourse(course)
courseBrowserPage.selectPeople()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt
index a424546711..9bb2940fa9 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt
@@ -18,12 +18,11 @@ package com.instructure.student.ui.e2e.offline
import android.util.Log
import androidx.test.espresso.Espresso
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.UiDevice
import com.google.android.material.checkbox.MaterialCheckBox
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.OfflineE2E
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.student.ui.e2e.offline.utils.OfflineTestUtils
@@ -36,26 +35,26 @@ import org.junit.Test
@HiltAndroidTest
class OfflineSyncProgressE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@OfflineE2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SYNC_PROGRESS, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SYNC_PROGRESS, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
fun testOfflineGlobalCourseSyncProgressE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1)
+ val data = seedData(students = 1, teachers = 1, courses = 4, announcements = 3, discussions = 5, syllabusBody = "Syllabus body")
val student = data.studentsList[0]
val course1 = data.coursesList[0]
val course2 = data.coursesList[1]
+ val course3 = data.coursesList[2]
+ val course4 = data.coursesList[3]
val testAnnouncement = data.announcementsList[0]
- Log.d(PREPARATION_TAG, "Get the device to be able to perform app-independent actions on it.")
- val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-
- Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG,"Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
dashboardPage.waitForRender()
@@ -67,41 +66,55 @@ class OfflineSyncProgressE2ETest : StudentTest() {
Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.")
manageOfflineContentPage.changeItemSelectionState(course1.name)
+ manageOfflineContentPage.changeItemSelectionState(course2.name)
+ manageOfflineContentPage.changeItemSelectionState(course3.name)
manageOfflineContentPage.clickOnSyncButtonAndConfirm()
- Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.")
+ Log.d(STEP_TAG, "Wait for the Dashboard to be rendered.")
dashboardPage.waitForRender()
- dashboardPage.waitForSyncProgressDownloadStartedNotification()
- dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear()
- Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and click on it to enter the Sync Progress Page.")
- dashboardPage.waitForSyncProgressStartingNotification()
+ Log.d(STEP_TAG, "Click on the Dashboard notification to open the Sync Progress Page.")
dashboardPage.clickOnSyncProgressNotification()
- Log.d(STEP_TAG, "Assert that the Sync Progress has started.")
- syncProgressPage.waitForDownloadStarting()
-
Log.d(STEP_TAG, "Assert that the Sync Progress has been successful (so to have the success title and the course success indicator).")
syncProgressPage.assertDownloadProgressSuccessDetails()
syncProgressPage.assertCourseSyncedSuccessfully(course1.name)
+ syncProgressPage.assertCourseSyncedSuccessfully(course2.name)
+ syncProgressPage.assertCourseSyncedSuccessfully(course3.name)
+
+ Log.d(STEP_TAG, "Get the sum of '${course1.name}', '${course2.name}' and '${course3.name}' courses' sizes and assert that the sum number is displayed under the progress bar.")
+ val sumOfSyncedCourseSizes = syncProgressPage.getCourseSize(course1.name) + syncProgressPage.getCourseSize(course2.name) + syncProgressPage.getCourseSize(course3.name)
+ syncProgressPage.assertSumOfCourseSizes(sumOfSyncedCourseSizes)
+
+ Log.d(STEP_TAG, "Expand '${course1.name}' course and assert a few tabs (for example) to ensure they synced well and the success indicator is displayed in their rows.")
+ syncProgressPage.expandCollapseCourse(course1.name)
+ syncProgressPage.assertCourseTabSynced("Syllabus")
+ syncProgressPage.assertCourseTabSynced("Announcements")
+ syncProgressPage.assertCourseTabSynced("Grades")
+ device.waitForIdle()
Log.d(STEP_TAG, "Navigate back to Dashboard Page and wait for it to be rendered.")
Espresso.pressBack()
+ Log.d(STEP_TAG, "Assert that the offline sync icon is displayed in online mode on the synced courses' course cards.")
+ dashboardPage.assertCourseOfflineSyncIconVisible(course1.name)
+ dashboardPage.assertCourseOfflineSyncIconVisible(course2.name)
+
Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.")
turnOffConnectionViaADB()
- Thread.sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script.
- device.waitForIdle()
- device.waitForWindowUpdate(null, 10000)
+ OfflineTestUtils.waitForNetworkToGoOffline(device)
+ Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.")
dashboardPage.waitForRender()
Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.")
OfflineTestUtils.assertOfflineIndicator()
- Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced course's course card.")
+ Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced courses' course card.")
dashboardPage.assertCourseOfflineSyncIconVisible(course1.name)
- dashboardPage.assertCourseOfflineSyncIconGone(course2.name)
+ dashboardPage.assertCourseOfflineSyncIconVisible(course2.name)
+ dashboardPage.assertCourseOfflineSyncIconVisible(course3.name)
+ dashboardPage.assertCourseOfflineSyncIconGone(course4.name)
Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.")
dashboardPage.selectCourse(course1)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt
index 67bae6de61..6efa56db19 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt
@@ -20,6 +20,7 @@ import android.util.Log
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.OfflineE2E
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.pandautils.R
@@ -33,20 +34,21 @@ import org.junit.Test
@HiltAndroidTest
class OfflineSyncSettingsE2ETest : StudentTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@OfflineE2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.OFFLINE_CONTENT, TestCategory.E2E)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.OFFLINE_CONTENT, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
fun offlineSyncSettingsE2ETest() {
Log.d(PREPARATION_TAG,"Seeding data.")
val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1)
val student = data.studentsList[0]
- Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG,"Login with user: '${student.name}', login id: '${student.loginId}'.")
tokenLogin(student)
dashboardPage.waitForRender()
@@ -110,13 +112,13 @@ class OfflineSyncSettingsE2ETest : StudentTest() {
Log.d(STEP_TAG, "Click 'Find My School' button.")
loginLandingPage.clickFindMySchoolButton()
- Log.d(STEP_TAG, "Enter domain: ${student.domain}.")
+ Log.d(STEP_TAG, "Enter domain: '${student.domain}'.")
loginFindSchoolPage.enterDomain(student.domain)
Log.d(STEP_TAG, "Click on 'Next' button on the Toolbar.")
loginFindSchoolPage.clickToolbarNextMenuItem()
- Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.")
loginSignInPage.loginAs(student)
dashboardPage.waitForRender()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt
index 0696eec9b3..c7dbd9c204 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt
@@ -78,4 +78,10 @@ object OfflineTestUtils {
hasSibling(withId(R.id.topPanel) +
hasDescendant(withText(R.string.noInternetConnectionTitle))))).click()
}
+
+ fun waitForNetworkToGoOffline(device: UiDevice) {
+ Thread.sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script.
+ device.waitForIdle()
+ device.waitForWindowUpdate(null, 10000)
+ }
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/usergroups/UserGroupFilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/usergroups/UserGroupFilesE2ETest.kt
index be17d15451..1c40730be3 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/usergroups/UserGroupFilesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/usergroups/UserGroupFilesE2ETest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 - present Instructure, Inc.
+ * Copyright (C) 2023 - 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.
@@ -20,12 +20,12 @@ import android.util.Log
import androidx.test.espresso.Espresso
import androidx.test.espresso.intent.Intents
import com.instructure.canvas.espresso.E2E
-import com.instructure.dataseeding.api.GroupsApi
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.dataseeding.api.GroupsApi
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
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 38edfa87c9..3d89f7af28 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
@@ -20,6 +20,7 @@ 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
@@ -29,6 +30,7 @@ import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CourseSettings
import com.instructure.canvasapi2.utils.toApiString
import com.instructure.dataseeding.model.SubmissionType
+import com.instructure.student.R
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.routeTo
import com.instructure.student.ui.utils.tokenLogin
@@ -385,6 +387,155 @@ class AssignmentDetailsInteractionTest : StudentTest() {
assignmentDetailsPage.assertStatusSubmitted()
}
+ @Test
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
+ fun testReminderSectionIsNotVisibleWhenThereIsNoFutureDueDate() {
+ val data = setUpData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, -1)
+ }.time.toApiString())
+ goToAssignmentList()
+
+ assignmentListPage.clickAssignment(assignment)
+
+ assignmentDetailsPage.assertReminderSectionNotDisplayed()
+ }
+
+ @Test
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
+ fun testReminderSectionIsVisibleWhenThereIsFutureDueDate() {
+ val data = setUpData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 1)
+ }.time.toApiString())
+ goToAssignmentList()
+
+ assignmentListPage.clickAssignment(assignment)
+
+ assignmentDetailsPage.assertReminderSectionDisplayed()
+ }
+
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
+ fun testAddReminder() {
+ val data = setUpData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 1)
+ }.time.toApiString())
+ goToAssignmentList()
+
+ assignmentListPage.clickAssignment(assignment)
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+
+ assignmentDetailsPage.assertReminderDisplayedWithText("1 Hour Before")
+ }
+
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
+ fun testRemoveReminder() {
+ val data = setUpData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 1)
+ }.time.toApiString())
+ goToAssignmentList()
+
+ assignmentListPage.clickAssignment(assignment)
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+
+ assignmentDetailsPage.assertReminderDisplayedWithText("1 Hour Before")
+
+ assignmentDetailsPage.removeReminderWithText("1 Hour Before")
+
+ assignmentDetailsPage.assertReminderNotDisplayedWithText("1 Hour Before")
+ }
+
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
+ fun testAddCustomReminder() {
+ val data = setUpData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 1)
+ }.time.toApiString())
+ goToAssignmentList()
+
+ assignmentListPage.clickAssignment(assignment)
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.clickCustom()
+ assignmentDetailsPage.assertDoneButtonIsDisabled()
+ assignmentDetailsPage.fillQuantity("15")
+ assignmentDetailsPage.assertDoneButtonIsDisabled()
+ assignmentDetailsPage.clickHoursBefore()
+ assignmentDetailsPage.clickDone()
+
+ assignmentDetailsPage.assertReminderDisplayedWithText("15 Hours Before")
+ }
+
+ @Test
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
+ fun testAddReminderInPastShowsError() {
+ val data = setUpData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.MINUTE, 30)
+ }.time.toApiString())
+ goToAssignmentList()
+
+ assignmentListPage.clickAssignment(assignment)
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+
+ checkToastText(R.string.reminderInPast, activityRule.activity)
+ }
+
+ @Test
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
+ fun testAddReminderForTheSameTimeShowsError() {
+ val data = setUpData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 1)
+ }.time.toApiString())
+ goToAssignmentList()
+
+ assignmentListPage.clickAssignment(assignment)
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+
+ checkToastText(R.string.reminderAlreadySet, activityRule.activity)
+ }
+
+ @Test
+ @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
+ fun testAddReminderForTheSameTimeWithDifferentMeasureOfTimeShowsError() {
+ val data = setUpData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 10)
+ }.time.toApiString())
+ goToAssignmentList()
+
+ assignmentListPage.clickAssignment(assignment)
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Week Before")
+ assignmentDetailsPage.clickAddReminder()
+
+ assignmentDetailsPage.clickCustom()
+ assignmentDetailsPage.fillQuantity("7")
+ assignmentDetailsPage.clickDaysBefore()
+ assignmentDetailsPage.clickDone()
+
+ checkToastText(R.string.reminderAlreadySet, activityRule.activity)
+ }
+
private fun setUpData(restrictQuantitativeData: Boolean = false): MockCanvas {
// Test clicking on the Submission and Rubric button to load the Submission Details Page
val data = MockCanvas.init(
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt
index 4f0f1f8db9..e1762cd4be 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt
@@ -34,7 +34,7 @@ class ElementaryDashboardInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testNavigateToElementaryDashboard() {
// User should be able to tap and navigate to dashboard page
goToElementaryDashboard(courseCount = 1, favoriteCourseCount = 1)
@@ -46,7 +46,7 @@ class ElementaryDashboardInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testTabsNavigation() {
goToElementaryDashboard(courseCount = 1, favoriteCourseCount = 1)
elementaryDashboardPage.assertElementaryTabVisibleAndSelected(ElementaryDashboardPage.ElementaryTabType.HOMEROOM)
@@ -70,7 +70,7 @@ class ElementaryDashboardInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOnlyElementarySpecificNavigationItemsShownInTheNavigationDrawer() {
goToElementaryDashboard(courseCount = 1, favoriteCourseCount = 1)
elementaryDashboardPage.openDrawer()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt
index 36923ce040..bad0d87003 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt
@@ -39,7 +39,7 @@ class ElementaryGradesInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testShowGrades() {
val data = createMockData(courseCount = 3)
goToGradesTab(data)
@@ -52,7 +52,7 @@ class ElementaryGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testRefresh() {
val data = createMockData(courseCount = 1)
goToGradesTab(data)
@@ -72,7 +72,7 @@ class ElementaryGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOpenCourseGrades() {
val data = createMockData(courseCount = 3)
goToGradesTab(data)
@@ -90,7 +90,7 @@ class ElementaryGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testChangeGradingPeriod() {
val data = createMockData(courseCount = 3, withGradingPeriods = true)
goToGradesTab(data)
@@ -104,7 +104,7 @@ class ElementaryGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.COMMON, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testEmptyView() {
val data = createMockData(homeroomCourseCount = 1)
goToGradesTab(data)
@@ -114,7 +114,7 @@ class ElementaryGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.COMMON, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testShowPercentageOnlyIfNoAlphabeticalGrade() {
val data = createMockData(courseCount = 1)
goToGradesTab(data)
@@ -135,7 +135,7 @@ class ElementaryGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.COMMON, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testDontShowProgressWhenQuantitativeDataIsRestricted() {
val data = createMockData(courseCount = 1)
goToGradesTab(data)
@@ -157,7 +157,7 @@ class ElementaryGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.COMMON, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testDontShowGradeWhenQuantitativeDataIsRestrictedAndThereIsOnlyScore() {
val data = createMockData(courseCount = 1)
goToGradesTab(data)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt
index fa08c724fc..fc2ed7ace6 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt
@@ -41,7 +41,7 @@ class HomeroomInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testAnnouncementsAndCoursesShowUpOnHomeroom() {
val data = createMockDataWithHomeroomCourse(courseCount = 3)
val homeroomCourse = data.courses.values.first { it.homeroomCourse }
@@ -66,7 +66,7 @@ class HomeroomInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOnlyCoursesShowUpOnHomeroomIfNoHomeroomAnnouncement() {
val data = createMockDataWithHomeroomCourse(courseCount = 3)
@@ -87,7 +87,7 @@ class HomeroomInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOnlyAnnouncementShowsUpOnHomeroomIfNoCourses() {
val data = createMockDataWithHomeroomCourse()
val homeroomCourse = data.courses.values.first { it.homeroomCourse }
@@ -106,7 +106,7 @@ class HomeroomInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOpenCourse() {
val data = createMockDataWithHomeroomCourse(courseCount = 3)
val homeroomCourse = data.courses.values.first { it.homeroomCourse }
@@ -127,7 +127,7 @@ class HomeroomInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testRefreshAfterEnrolledToCourses() {
val data = createMockDataWithHomeroomCourse()
@@ -160,7 +160,7 @@ class HomeroomInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOpenHomeroomCourseAnnouncements() {
val data = createMockDataWithHomeroomCourse(courseCount = 3, homeroomCourseCount = 2)
val homeroomCourse = data.courses.values.first { it.homeroomCourse }
@@ -184,7 +184,7 @@ class HomeroomInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOpenCourseAnnouncements() {
val data = createMockDataWithHomeroomCourse(courseCount = 1)
@@ -203,7 +203,7 @@ class HomeroomInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testShowCourseCardWithAnnouncement() {
val data = createMockDataWithHomeroomCourse(courseCount = 3)
val homeroomCourse = data.courses.values.first { it.homeroomCourse }
@@ -224,7 +224,7 @@ class HomeroomInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testDueTodayAndMissingAssignments() {
val data = createMockDataWithHomeroomCourse(courseCount = 1)
val homeroomCourse = data.courses.values.first { it.homeroomCourse }
@@ -249,7 +249,7 @@ class HomeroomInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOpenAssignments() {
val data = createMockDataWithHomeroomCourse(courseCount = 1)
val homeroomCourse = data.courses.values.first { it.homeroomCourse }
@@ -272,7 +272,7 @@ class HomeroomInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.COMMON, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testEmptyState() {
val data = createMockDataWithHomeroomCourse()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt
index 53b95676a4..2262539a98 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt
@@ -44,7 +44,7 @@ class ImportantDatesInteractionTest : StudentTest() {
@Test
@StubTablet(description = "The UI is different on tablet, so we only check the phone version")
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testShowCalendarEvents() {
val data = createMockData(courseCount = 1)
val course = data.courses.values.toList()[0]
@@ -59,7 +59,7 @@ class ImportantDatesInteractionTest : StudentTest() {
@Test
@StubTablet(description = "The UI is different on tablet, so we only check the phone version")
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testShowAssignment() {
val data = createMockData(courseCount = 1)
val course = data.courses.values.toList()[0]
@@ -75,7 +75,7 @@ class ImportantDatesInteractionTest : StudentTest() {
@Test
@StubTablet(description = "The UI is different on tablet, so we only check the phone version")
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testEmptyView() {
val data = createMockData(courseCount = 1)
@@ -86,7 +86,7 @@ class ImportantDatesInteractionTest : StudentTest() {
@Test
@StubTablet(description = "The UI is different on tablet, so we only check the phone version")
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testPullToRefresh() {
val data = createMockData(courseCount = 1)
val course = data.courses.values.toList()[0]
@@ -107,7 +107,7 @@ class ImportantDatesInteractionTest : StudentTest() {
@Test
@StubTablet(description = "The UI is different on tablet, so we only check the phone version")
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOpenCalendarEvent() {
val data = createMockData(courseCount = 1)
val course = data.courses.values.toList()[0]
@@ -127,7 +127,7 @@ class ImportantDatesInteractionTest : StudentTest() {
@Test
@StubTablet(description = "The UI is different on tablet, so we only check the phone version")
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOpenAssignment() {
val data = createMockData(courseCount = 1)
val course = data.courses.values.toList()[0]
@@ -147,7 +147,7 @@ class ImportantDatesInteractionTest : StudentTest() {
@Test
@StubTablet(description = "The UI is different on tablet, so we only check the phone version")
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testShowMultipleCalendarEventsOnSameDay() {
val data = createMockData(courseCount = 1)
val course = data.courses.values.toList()[0]
@@ -171,7 +171,7 @@ class ImportantDatesInteractionTest : StudentTest() {
@Test
@StubTablet(description = "The UI is different on tablet, so we only check the phone version")
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testMultipleCalendarEventsOnDifferentDays() {
val data = createMockData(courseCount = 1)
val course = data.courses.values.toList()[0]
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt
index 98af62da5a..489c813e44 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt
@@ -207,6 +207,7 @@ class InAppUpdateInteractionTest : StudentTest() {
@Test
fun showImmediateFlow() {
+ updatePrefs.clearPrefs()
with(appUpdateManager) {
setUpdateAvailable(400)
setUpdatePriority(4)
@@ -270,8 +271,8 @@ class InAppUpdateInteractionTest : StudentTest() {
}
@Test
- @Stub("Unstable, there is a ticket to fix this")
fun flexibleUpdateCompletesIfAppRestarts() {
+ updatePrefs.clearPrefs()
with(appUpdateManager) {
setUpdateAvailable(400)
setUpdatePriority(2)
@@ -288,6 +289,7 @@ class InAppUpdateInteractionTest : StudentTest() {
@Test
fun immediateUpdateCompletion() {
+ updatePrefs.clearPrefs()
with(appUpdateManager) {
setUpdateAvailable(400)
setUpdatePriority(4)
@@ -307,6 +309,7 @@ class InAppUpdateInteractionTest : StudentTest() {
@Test
fun hideImmediateUpdateFlowIfUserCancels() {
+ updatePrefs.clearPrefs()
with(appUpdateManager) {
setUpdateAvailable(400)
setUpdatePriority(4)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt
index 42c1233741..c33e422e9c 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt
@@ -38,7 +38,7 @@ class ResourcesInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testImportantLinksAndActionItemsShowUpInResourcesScreen() {
val data = createMockDataWithHomeroomCourse(courseCount = 2)
@@ -67,7 +67,7 @@ class ResourcesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOnlyActionItemsShowIfSyllabusIsEmpty() {
val data = createMockDataWithHomeroomCourse(courseCount = 2)
@@ -95,7 +95,7 @@ class ResourcesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOnlyLtiToolsShowIfNoHomeroomCourse() {
val data = createMockDataWithHomeroomCourse(courseCount = 2, homeroomCourseCount = 0)
@@ -117,7 +117,7 @@ class ResourcesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testRefresh() {
val data = createMockDataWithHomeroomCourse(courseCount = 2, homeroomCourseCount = 0)
@@ -152,7 +152,7 @@ class ResourcesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOpenLtiToolShowsCourseSelector() {
val data = createMockDataWithHomeroomCourse(courseCount = 2)
@@ -175,7 +175,7 @@ class ResourcesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOpenComposeMessageScreen() {
val data = createMockDataWithHomeroomCourse(courseCount = 2)
@@ -201,7 +201,7 @@ class ResourcesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.COMMON, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testImportantLinksForTwoCourses() {
val data = createMockDataWithHomeroomCourse(courseCount = 2)
@@ -225,7 +225,7 @@ class ResourcesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.COMMON, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testEmptyState() {
val data = createMockDataWithHomeroomCourse(courseCount = 2, homeroomCourseCount = 0)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt
index 2cfe7c7cbb..79f23cc8c2 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt
@@ -49,7 +49,7 @@ class ScheduleInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testShowCorrectHeaderItems() {
setDate(2021, Calendar.AUGUST, 11)
val data = createMockData(courseCount = 1)
@@ -74,7 +74,7 @@ class ScheduleInteractionTest : StudentTest() {
@Test
@StubLandscape(description = "This is intentionally stubbed on landscape mode because the item view is too narrow, but that's not a bug, it's intentional.")
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testShowScheduledAssignments() {
setDate(2021, Calendar.AUGUST, 11)
val data = createMockData(courseCount = 1)
@@ -93,7 +93,7 @@ class ScheduleInteractionTest : StudentTest() {
@Test
@StubLandscape(description = "This is intentionally stubbed on landscape mode because the item view is too narrow, but that's not a bug, it's intentional.")
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testShowMissingAssignments() {
setDate(2021, Calendar.AUGUST, 11)
val data = createMockData(courseCount = 1)
@@ -105,11 +105,11 @@ class ScheduleInteractionTest : StudentTest() {
goToScheduleTab(data)
schedulePage.scrollToPosition(12)
- schedulePage.assertMissingItemDisplayed(assignment1.name!!, courses[0].name, "10 pts")
+ schedulePage.assertMissingItemDisplayedInMissingItemSummary(assignment1.name!!, courses[0].name, "10 pts")
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testShowToDoEvents() {
setDate(2021, Calendar.AUGUST, 11)
val data = createMockData(courseCount = 1)
@@ -125,7 +125,7 @@ class ScheduleInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testRefresh() {
setDate(2021, Calendar.AUGUST, 11)
val data = createMockData(courseCount = 1)
@@ -154,7 +154,7 @@ class ScheduleInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testGoBack2Weeks() {
setDate(2021, Calendar.AUGUST, 11)
val data = createMockData(courseCount = 1)
@@ -177,7 +177,7 @@ class ScheduleInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testGoForward2Weeks() {
setDate(2021, Calendar.AUGUST, 11)
val data = createMockData(courseCount = 1)
@@ -200,7 +200,7 @@ class ScheduleInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOpenAssignment() {
setDate(2021, Calendar.AUGUST, 11)
val data = createMockData(courseCount = 1)
@@ -220,7 +220,7 @@ class ScheduleInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testOpenCourse() {
setDate(2021, Calendar.AUGUST, 11)
val data = createMockData(courseCount = 1)
@@ -239,7 +239,7 @@ class ScheduleInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.COMMON, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testMarkAsDone() {
setDate(2021, Calendar.AUGUST, 11)
val data = createMockData(courseCount = 1)
@@ -256,7 +256,7 @@ class ScheduleInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ @TestMetaData(Priority.COMMON, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.INTERACTION)
fun testTodayButton() {
setDate(2021, Calendar.AUGUST, 11)
val data = createMockData(courseCount = 1)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt
index 13ab1f1da3..87e4338e7b 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt
@@ -24,6 +24,7 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import com.instructure.canvas.espresso.Stub
+import com.instructure.canvas.espresso.StubCoverage
import com.instructure.canvas.espresso.StubTablet
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addAssignment
@@ -233,6 +234,7 @@ class ShareExtensionInteractionTest : StudentTest() {
}
@Test
+ @StubCoverage("Cannot init FileUploadWorker and OfflineSyncWorker")
fun testFileAssignmentSubmission() {
val data = createMockData()
val student = data.students[0]
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt
index 2e579c8a33..b5493d3b83 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt
@@ -26,14 +26,16 @@ import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasType
+import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
-import com.instructure.canvas.espresso.Stub
-import com.instructure.canvas.espresso.mockCanvas.MockCanvas
-import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.Priority
+import com.instructure.canvas.espresso.Stub
+import com.instructure.canvas.espresso.StubCoverage
import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.pandautils.utils.Const
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
@@ -91,6 +93,7 @@ class UserFilesInteractionTest : StudentTest() {
// Should be able to upload a file from the user's device
// Mocks the result from the expected intent, then uploads it.
@Test
+ @StubCoverage("Cannot init FileUploadWorker and OfflineSyncWorker")
@TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION)
fun testUpload_deviceFile() {
goToFilePicker()
@@ -120,6 +123,7 @@ class UserFilesInteractionTest : StudentTest() {
// Should be able to upload a file from the camera
// Mocks the result from the expected intent, then uploads it.
@Test
+ @StubCoverage("Cannot init FileUploadWorker and OfflineSyncWorker")
@TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION)
fun testUpload_fileFromCamera() {
@@ -161,6 +165,7 @@ class UserFilesInteractionTest : StudentTest() {
// Should be able to upload a file from the user's photo gallery
// Mocks the result from the expected intent, then uploads it.
@Test
+ @StubCoverage("Cannot init FileUploadWorker and OfflineSyncWorker")
@TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION)
fun testUpload_gallery() {
goToFilePicker()
@@ -222,6 +227,9 @@ class UserFilesInteractionTest : StudentTest() {
// Set up some rudimentary mock data, navigate to the file list page, then
// initiate a file upload
private fun goToFilePicker() : MockCanvas {
+
+ File(InstrumentationRegistry.getInstrumentation().targetContext.cacheDir, "file_upload").deleteRecursively()
+
val data = MockCanvas.init(
courseCount = 1,
favoriteCourseCount = 1,
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt
index 5ac189a0f8..871d533d39 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt
@@ -20,13 +20,18 @@ import android.view.View
import android.widget.ScrollView
import androidx.appcompat.widget.AppCompatButton
import androidx.test.espresso.Espresso
+import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
+import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import com.instructure.canvas.espresso.CanvasTest
import com.instructure.canvas.espresso.containsTextCaseInsensitive
import com.instructure.canvas.espresso.stringContainsTextCaseInsensitive
@@ -58,6 +63,8 @@ import com.instructure.espresso.waitForCheck
import com.instructure.student.R
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.anything
+import org.hamcrest.Matchers.not
open class AssignmentDetailsPage : BasePage(R.id.assignmentDetailsPage) {
val toolbar by OnViewWithId(R.id.toolbar)
@@ -239,6 +246,69 @@ open class AssignmentDetailsPage : BasePage(R.id.assignmentDetailsPage) {
fun assertSubmissionTypeDisplayed(submissionType: String) {
onView(withText(submissionType) + withAncestor(R.id.customPanel)).assertDisplayed()
}
+
+ fun assertReminderSectionNotDisplayed() {
+ onView(withId(R.id.reminderTitle)).assertNotDisplayed()
+ onView(withId(R.id.reminderDescription)).assertNotDisplayed()
+ onView(withId(R.id.reminderAdd)).assertNotDisplayed()
+ }
+
+ fun assertReminderSectionDisplayed() {
+ onView(withId(R.id.reminderTitle)).scrollTo().assertDisplayed()
+ onView(withId(R.id.reminderDescription)).scrollTo().assertDisplayed()
+ onView(withId(R.id.reminderAdd)).scrollTo().assertDisplayed()
+ }
+
+ fun clickAddReminder() {
+ onView(withId(R.id.reminderAdd)).scrollTo().click()
+ }
+
+ fun selectTimeOption(timeOption: String) {
+ onView(withText(timeOption)).scrollTo().click()
+ }
+
+ fun assertReminderDisplayedWithText(text: String) {
+ onView(withText(text)).scrollTo().assertDisplayed()
+ }
+
+ fun removeReminderWithText(text: String) {
+ onView(
+ allOf(
+ withId(R.id.remove),
+ hasSibling(withText(text))
+ )
+ ).click()
+ onView(withText(R.string.yes)).scrollTo().click()
+ }
+
+ fun assertReminderNotDisplayedWithText(text: String) {
+ onView(withText(text)).check(doesNotExist())
+ }
+
+ fun clickCustom() {
+ onData(anything()).inRoot(isDialog()).atPosition(6).perform(click())
+ }
+
+ fun fillQuantity(quantity: String) {
+ onView(withId(R.id.quantity)).scrollTo().typeText(quantity)
+ Espresso.closeSoftKeyboard()
+ }
+
+ fun clickHoursBefore() {
+ onView(withId(R.id.hours)).scrollTo().click()
+ }
+
+ fun clickDaysBefore() {
+ onView(withId(R.id.days)).scrollTo().click()
+ }
+
+ fun assertDoneButtonIsDisabled() {
+ onView(withText(R.string.done)).check(matches(not(isEnabled())))
+ }
+
+ fun clickDone() {
+ onView(withText(R.string.done)).click()
+ }
}
/**
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt
index d7dd3b1541..4d40a43b7d 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt
@@ -47,18 +47,18 @@ object CollaborationsPage {
.checkRepeat(webMatches(getText(), containsString("Start a New Collaboration") ), 30)
}
- fun assertGoogleDocsChoicePresent() {
+ fun assertGoogleDocsChoicePresentAsDefaultOption() {
Web.onWebView(Matchers.allOf(withId(R.id.contentWebView), isDisplayed()))
.withElement(DriverAtoms.findElement(Locator.ID, "collaboration_collaboration_type"))
.perform(DriverAtoms.webScrollIntoView())
.check(webMatches(getText(), containsString("Google Docs") ))
}
- fun assertGoogleDocsExplanationPresent() {
+ fun assertGoogleDocsWarningDescriptionPresent() {
Web.onWebView(Matchers.allOf(withId(R.id.contentWebView), isDisplayed()))
.withElement(DriverAtoms.findElement(Locator.ID, "google_docs_description"))
.perform(DriverAtoms.webScrollIntoView())
- .check(webMatches(getText(), containsString("Google Docs is a great place to collaborate") ))
+ .check(webMatches(getText(), containsString("Warning:") ))
}
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt
index 18971f74a5..4ede8e10ec 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt
@@ -27,7 +27,6 @@ import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
-import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.platform.app.InstrumentationRegistry
import com.instructure.canvas.espresso.scrollRecyclerView
@@ -43,6 +42,7 @@ import com.instructure.espresso.page.*
import com.instructure.student.R
import com.instructure.student.ui.utils.ViewUtils
import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matcher
import org.hamcrest.Matchers
@@ -359,7 +359,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
}
//OfflineMethod
- fun waitForNetworkComeBack() {
+ fun waitForOfflineIndicatorNotDisplayed() {
assertDisplaysCourses()
retry(times = 5, delay = 2000) {
assertOfflineIndicatorNotDisplayed()
@@ -367,13 +367,22 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
}
//OfflineMethod
- fun waitForNetworkOff() {
+ fun waitForOfflineIndicatorDisplayed() {
assertDisplaysCourses()
retry(times = 5, delay = 2000) {
assertOfflineIndicatorDisplayed()
}
}
+ //OfflineMethod
+ fun waitForOfflineSyncDashboardNotifications() {
+ waitForSyncProgressDownloadStartedNotification()
+ waitForSyncProgressDownloadStartedNotificationToDisappear()
+
+ waitForSyncProgressStartingNotification()
+ waitForSyncProgressStartingNotificationToDisappear()
+ }
+
//OfflineMethod
fun assertCourseOfflineSyncIconVisible(courseName: String) {
waitForView(withId(R.id.offlineSyncIcon) + hasSibling(withId(R.id.titleTextView) + withText(courseName))).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@@ -386,7 +395,8 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
//OfflineMethod
fun clickOnSyncProgressNotification() {
- waitForView(ViewMatchers.withText(com.instructure.pandautils.R.string.syncProgress_syncingOfflineContent)).click()
+ Thread.sleep(2500)
+ onView(anyOf(withText(R.string.syncProgress_syncQueued),withText(R.string.syncProgress_downloadStarting), withText(R.string.syncProgress_syncingOfflineContent))).click()
}
//OfflineMethod
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt
index 364b5ac258..026f1f3f52 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt
@@ -23,7 +23,6 @@ import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast
-import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
@@ -47,6 +46,8 @@ import com.instructure.espresso.page.BasePage
import com.instructure.espresso.page.plus
import com.instructure.espresso.page.waitForViewWithId
import com.instructure.espresso.page.withAncestor
+import com.instructure.espresso.page.withId
+import com.instructure.espresso.page.withText
import com.instructure.espresso.scrollTo
import com.instructure.student.R
import com.instructure.student.ui.utils.TypeInRCETextEditor
@@ -91,7 +92,7 @@ class DiscussionDetailsPage : BasePage(R.id.discussionDetailsPage) {
onView(withId(R.id.contentWebView) + withAncestor(R.id.discussionRepliesWebViewWrapper)).scrollTo()
}
- private fun clickReply() {
+ fun clickReply() {
replyButton.click()
}
@@ -118,11 +119,15 @@ class DiscussionDetailsPage : BasePage(R.id.discussionDetailsPage) {
fun sendReply(replyMessage: String) {
clickReply()
waitForViewWithId(R.id.rce_webView).perform(TypeInRCETextEditor(replyMessage))
- onView(withId(R.id.menu_send)).click()
+ clickOnSendReplyButton()
sleep(3000) // wait out the toast message
}
+ private fun clickOnSendReplyButton() {
+ onView(withId(R.id.menu_send)).click()
+ }
+
fun assertReplyDisplayed(reply: DiscussionEntry, refreshesAllowed: Int = 0) {
// Allow up to refreshesAllowed attempt/refresh cycles
@@ -314,6 +319,16 @@ class DiscussionDetailsPage : BasePage(R.id.discussionDetailsPage) {
onView(withId(R.id.pointsTextView)).assertNotDisplayed()
}
+ fun clickOnInnerReply() {
+ onWebView(withId(R.id.contentWebView) + withAncestor(R.id.discussionRepliesWebViewWrapper))
+ .withElement(findElement(Locator.XPATH, "//div[@class='reply_wrapper' and contains(@id, 'reply')]"))
+ .perform(webClick())
+ }
+
+ fun clickOnAddBookmarkMenu() {
+ onView(withText("Add Bookmark")).click()
+ }
+
private fun isUnreadIndicatorVisible(reply: DiscussionEntry): Boolean {
return try {
onWebView(withId(R.id.contentWebView) + withAncestor(R.id.discussionRepliesWebViewWrapper))
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt
index 28f125e1a9..83a593b0a2 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt
@@ -16,7 +16,6 @@
*/
package com.instructure.student.ui.pages
-import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.platform.app.InstrumentationRegistry
@@ -25,6 +24,7 @@ import com.instructure.canvas.espresso.explicitClick
import com.instructure.canvas.espresso.scrollRecyclerView
import com.instructure.canvas.espresso.waitForMatcherWithRefreshes
import com.instructure.canvasapi2.models.DiscussionTopicHeader
+import com.instructure.espresso.DoesNotExistAssertion
import com.instructure.espresso.OnViewWithId
import com.instructure.espresso.RecyclerViewItemCountAssertion
import com.instructure.espresso.Searchable
@@ -57,7 +57,6 @@ open class DiscussionListPage(val searchable: Searchable) : BasePage(R.id.discus
fun waitForDiscussionTopicToDisplay(topicTitle: String) {
val matcher = allOf(withText(topicTitle), withId(R.id.discussionTitle))
waitForView(matcher)
-
}
fun assertTopicDisplayed(topicTitle: String) {
@@ -67,7 +66,7 @@ open class DiscussionListPage(val searchable: Searchable) : BasePage(R.id.discus
}
fun assertTopicNotDisplayed(topicTitle: String?) {
- onView(allOf(withText(topicTitle))).check(ViewAssertions.doesNotExist())
+ onView(allOf(withText(topicTitle))).check(DoesNotExistAssertion(5))
}
fun assertEmpty() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt
index 158d895e8b..752a4875cc 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt
@@ -16,11 +16,9 @@
*/
package com.instructure.student.ui.pages
-import android.widget.Button
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.matcher.ViewMatchers
-import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.withId
import com.instructure.canvas.espresso.containsTextCaseInsensitive
import com.instructure.espresso.OnViewWithId
@@ -34,7 +32,6 @@ import com.instructure.espresso.page.withDescendant
import com.instructure.espresso.page.withText
import com.instructure.espresso.scrollTo
import com.instructure.student.R
-import org.hamcrest.core.AllOf.allOf
class FileUploadPage : BasePage() {
private val cameraButton by OnViewWithId(R.id.fromCamera)
@@ -56,7 +53,7 @@ class FileUploadPage : BasePage() {
}
fun clickUpload() {
- onView(allOf(isAssignableFrom(Button::class.java), withText(R.string.upload))).click()
+ onView(withText(R.string.upload)).click()
}
fun clickTurnIn() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt
index 959dee5ad9..bb24ae21e7 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt
@@ -126,7 +126,7 @@ class InboxConversationPage : BasePage(R.id.inboxConversationPage) {
}
fun assertNoSubjectDisplayed() {
- onView(withId(R.id.subjectView) + withText(R.string.noSubject)).assertDisplayed()
+ onView(withId(R.id.subjectView) + withParent(withId(R.id.header)) + withText(R.string.noSubject)).assertDisplayed()
}
fun refresh() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt
index 8626f8d2a0..f3fa937f0e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt
@@ -300,4 +300,8 @@ class InboxPage : BasePage(R.id.inboxPage) {
editToolbar.assertVisibility(visibility)
}
+ fun assertConversationSubject(expectedSubject: String) {
+ onView(withId(R.id.subjectView) + withText(expectedSubject) + withAncestor(R.id.inboxRecyclerView)).assertDisplayed()
+ }
+
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt
index 2e38a15188..2fc890500e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt
@@ -20,6 +20,7 @@ import android.view.View
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import com.instructure.espresso.*
import com.instructure.espresso.page.*
import com.instructure.pandautils.binding.BindableViewHolder
@@ -81,13 +82,13 @@ class SchedulePage : BasePage(R.id.schedulePage) {
var i: Int = 0
while (true) {
scrollToPosition(i)
- Thread.sleep(500)
+ Thread.sleep(300)
try {
if(target == null) onView(withParent(itemId) + withText(itemName)).scrollTo()
else onView(target + withText(itemName)).scrollTo()
break
} catch(e: NoMatchingViewException) {
- i++
+ i+=2
}
}
}
@@ -100,16 +101,22 @@ class SchedulePage : BasePage(R.id.schedulePage) {
waitForView(withAncestor(R.id.plannerItems) + withText(scheduleItemName)).assertDisplayed()
}
- fun assertMissingItemDisplayed(itemName: String, courseName: String, pointsPossible: String) {
+ fun assertMissingItemDisplayedOnPlannerItem(itemName: String, courseName: String, pointsPossible: String) {
+ val titleMatcher = withId(R.id.title) + withText(itemName)
+ val courseNameMatcher = withId(R.id.scheduleCourseHeaderText) + withText(courseName)
+ val pointsPossibleMatcher = withId(R.id.points) + withText(pointsPossible)
+
+ onView(withId(R.id.plannerItems) + hasSibling(courseNameMatcher) + withDescendant(titleMatcher) + withDescendant(pointsPossibleMatcher) + withDescendant(withText(R.string.missingAssignment)))
+ .scrollTo()
+ .assertDisplayed()
+ }
+
+ fun assertMissingItemDisplayedInMissingItemSummary(itemName: String, courseName: String, pointsPossible: String) {
val titleMatcher = withId(R.id.title) + withText(itemName)
val courseNameMatcher = withId(R.id.courseName) + withText(courseName)
val pointsPossibleMatcher = withId(R.id.points) + withText(pointsPossible)
- onView(
- withId(R.id.missingItemLayout) + withDescendant(titleMatcher) + withDescendant(
- courseNameMatcher
- ) + withDescendant(pointsPossibleMatcher)
- )
+ onView(withId(R.id.missingItemLayout) + withDescendant(courseNameMatcher) + withDescendant(titleMatcher) + withDescendant(pointsPossibleMatcher))
.scrollTo()
.assertDisplayed()
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt
index f49c368035..dfb0cd3307 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt
@@ -17,12 +17,15 @@
package com.instructure.student.ui.pages.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.espresso.OnViewWithId
+import com.instructure.espresso.assertContainsText
import com.instructure.espresso.assertDisplayed
import com.instructure.espresso.assertVisibility
+import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
import com.instructure.espresso.page.onView
import com.instructure.espresso.page.plus
@@ -32,6 +35,7 @@ import com.instructure.espresso.page.withId
import com.instructure.espresso.page.withParent
import com.instructure.espresso.page.withText
import com.instructure.pandautils.R
+import com.instructure.student.ui.utils.getView
class SyncProgressPage : BasePage(R.id.syncProgressPage) {
@@ -55,4 +59,32 @@ class SyncProgressPage : BasePage(R.id.syncProgressPage) {
onView(withId(R.id.courseName) + withText(courseName) + withAncestor(R.id.syncProgressPage)).assertDisplayed()
onView(withId(R.id.successIndicator) + withParent(withId(R.id.actionContainer) + hasSibling(withId(R.id.courseName) + withText(courseName)))).assertVisibility(ViewMatchers.Visibility.VISIBLE)
}
+
+ fun expandCollapseCourse(courseName: String) {
+ onView(withId(R.id.toggleButton) + hasSibling(withId(R.id.courseName) + withText(courseName))).click()
+ }
+
+ fun assertCourseTabSynced(tabName: String) {
+ onView(withId(R.id.successIndicator) + withParent(withId(R.id.actionContainer) + hasSibling(withId(R.id.tabTitle) + withText(tabName)))).assertVisibility(ViewMatchers.Visibility.VISIBLE)
+ }
+
+ fun getCourseSize(courseName: String): Int {
+ val courseSizeView = onView(withId(R.id.courseSize) + hasSibling(withId(R.id.courseName) + withText(courseName)))
+ val courseSizeText = (courseSizeView.getView() as TextView).text.toString()
+ return courseSizeText.split(" ")[0].toInt()
+ }
+
+ fun assertSumOfCourseSizes(expectedSize: Int) {
+ if(expectedSize > 999) {
+ val convertedSumSize = convertKiloBytesToMegaBytes(expectedSize)
+ onView(withId(R.id.downloadProgressText)).assertContainsText(convertedSumSize.toString())
+ }
+ else {
+ onView(withId(R.id.downloadProgressText)).assertContainsText(expectedSize.toString())
+ }
+ }
+
+ private fun convertKiloBytesToMegaBytes(kilobytes: Int): Double {
+ return kilobytes / 1000.0
+ }
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt
new file mode 100644
index 0000000000..078ec98f70
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 - 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.utils
+
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import com.instructure.student.activity.LoginActivity
+import org.junit.Rule
+
+abstract class StudentComposeTest : StudentTest() {
+
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule()
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt
index ae17fa6e2a..02c6b4d5e4 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt
@@ -23,6 +23,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Environment
import androidx.fragment.app.FragmentActivity
+import androidx.test.espresso.Espresso
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
@@ -58,6 +59,10 @@ fun StudentTest.slowLogIn(enrollmentType: String = EnrollmentTypes.STUDENT_ENROL
return user
}
+fun StudentTest.openOverflowMenu() {
+ Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext)
+}
+
fun seedDataForK5(
teachers: Int = 0,
tas: Int = 0,
@@ -271,16 +276,7 @@ fun seedAssignmentSubmission(
it.attachmentsList.addAll(fileAttachments)
}
- // Seed the submissions
- val submissionRequest = SubmissionsApi.SubmissionSeedRequest(
- assignmentId = assignmentId,
- courseId = courseId,
- studentToken = studentToken,
- commentSeedsList = commentSeeds,
- submissionSeedsList = submissionSeeds
- )
-
- return SubmissionsApi.seedAssignmentSubmission(submissionRequest)
+ return SubmissionsApi.seedAssignmentSubmission(courseId, studentToken, assignmentId, commentSeeds, submissionSeeds)
}
fun uploadTextFile(
@@ -304,10 +300,11 @@ fun uploadTextFile(
// Start the Canvas file upload process
return FileUploadsApi.uploadFile(
- courseId,
- assignmentId,
- file.readBytes(),
- file.name,
- token,
- fileUploadType)
+ courseId,
+ assignmentId,
+ file.readBytes(),
+ file.name,
+ token,
+ fileUploadType
+ )
}
diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml
index 064bc4dabb..121b471855 100644
--- a/apps/student/src/main/AndroidManifest.xml
+++ b/apps/student/src/main/AndroidManifest.xml
@@ -40,6 +40,7 @@
+
-
+
@@ -296,6 +298,11 @@
+
+
+
{
@@ -214,7 +270,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
StudentLogoutTask(
LogoutTask.Type.LOGOUT,
typefaceBehavior = typefaceBehavior,
- databaseProvider = databaseProvider
+ databaseProvider = databaseProvider,
+ alarmScheduler = alarmScheduler
).execute()
}
.setNegativeButton(android.R.string.cancel, null)
@@ -325,6 +382,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
} catch {
firebaseCrashlytics.recordException(it)
}
+
+ scheduleAlarms()
}
private fun handleTokenCheck(online: Boolean?) {
@@ -334,7 +393,12 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
lifecycleScope.launch {
val isTokenValid = repository.isTokenValid()
if (!isTokenValid) {
- StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior, databaseProvider = databaseProvider).execute()
+ StudentLogoutTask(
+ LogoutTask.Type.LOGOUT,
+ typefaceBehavior = typefaceBehavior,
+ databaseProvider = databaseProvider,
+ alarmScheduler = alarmScheduler
+ ).execute()
}
}
}
@@ -387,7 +451,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
if (ApiPrefs.user == null ) {
// Hard case to repro but it's possible for a user to force exit the app before we finish saving the user but they will still launch into the app
// If that happens, log out
- StudentLogoutTask(LogoutTask.Type.LOGOUT, databaseProvider = databaseProvider).execute()
+ StudentLogoutTask(LogoutTask.Type.LOGOUT, databaseProvider = databaseProvider, alarmScheduler = alarmScheduler).execute()
}
setupBottomNavigation()
@@ -664,7 +728,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
override fun overrideFont() {
super.overrideFont()
- typefaceBehavior.overrideFont(navigationBehavior.fontFamily.fontPath)
+ typefaceBehavior.overrideFont(navigationBehavior.canvasFont)
}
//endregion
@@ -894,6 +958,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
private fun selectBottomNavFragment(fragmentClass: Class) {
val selectedFragment = supportFragmentManager.findFragmentByTag(fragmentClass.name)
+ (topFragment as? DashboardFragment)?.cancelCardDrag()
+
if (selectedFragment == null) {
val fragment = createBottomNavFragment(fragmentClass.name)
val newArguments = if (fragment?.arguments != null) fragment.requireArguments() else Bundle()
@@ -1215,6 +1281,12 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}
}
+ private fun scheduleAlarms() {
+ lifecycleScope.launch {
+ alarmScheduler.scheduleAllAlarmsForCurrentUser()
+ }
+ }
+
companion object {
fun createIntent(context: Context, route: Route): Intent {
return Intent(context, NavigationActivity::class.java).apply { putExtra(Route.ROUTE, route) }
diff --git a/apps/student/src/main/java/com/instructure/student/di/AlarmSchedulerModule.kt b/apps/student/src/main/java/com/instructure/student/di/AlarmSchedulerModule.kt
new file mode 100644
index 0000000000..ee3ad2f563
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/di/AlarmSchedulerModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 - 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.di
+
+import android.content.Context
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+class AlarmSchedulerModule {
+
+ @Provides
+ fun provideAlarmScheduler(@ApplicationContext context: Context, reminderDao: ReminderDao, apiPrefs: ApiPrefs): AlarmScheduler {
+ return AlarmScheduler(context, reminderDao, apiPrefs)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt b/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt
index 24914b2cd9..5672e1660e 100644
--- a/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt
+++ b/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt
@@ -20,6 +20,7 @@ import androidx.fragment.app.FragmentActivity
import com.instructure.loginapi.login.LoginNavigation
import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter
import com.instructure.pandautils.room.offline.DatabaseProvider
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
import com.instructure.student.features.login.StudentAcceptableUsePolicyRouter
import com.instructure.student.features.login.StudentLoginNavigation
import dagger.Module
@@ -32,12 +33,16 @@ import dagger.hilt.android.components.ActivityComponent
class LoginModule {
@Provides
- fun provideAcceptabelUsePolicyRouter(activity: FragmentActivity, databaseProvider: DatabaseProvider): AcceptableUsePolicyRouter {
- return StudentAcceptableUsePolicyRouter(activity, databaseProvider)
+ fun provideAcceptabelUsePolicyRouter(
+ activity: FragmentActivity,
+ databaseProvider: DatabaseProvider,
+ alarmScheduler: AlarmScheduler
+ ): AcceptableUsePolicyRouter {
+ return StudentAcceptableUsePolicyRouter(activity, databaseProvider, alarmScheduler)
}
@Provides
- fun provideLoginNavigation(activity: FragmentActivity, databaseProvider: DatabaseProvider): LoginNavigation {
- return StudentLoginNavigation(activity, databaseProvider)
+ fun provideLoginNavigation(activity: FragmentActivity, databaseProvider: DatabaseProvider, alarmScheduler: AlarmScheduler): LoginNavigation {
+ return StudentLoginNavigation(activity, databaseProvider, alarmScheduler)
}
}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentDetailsModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentDetailsModule.kt
index 05e0815954..36ec1daf56 100644
--- a/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentDetailsModule.kt
+++ b/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentDetailsModule.kt
@@ -18,6 +18,7 @@
package com.instructure.student.di.feature
import com.instructure.canvasapi2.apis.*
+import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
import com.instructure.pandautils.room.offline.daos.QuizDao
import com.instructure.pandautils.room.offline.facade.AssignmentFacade
import com.instructure.pandautils.room.offline.facade.CourseFacade
@@ -59,8 +60,9 @@ class AssignmentDetailsModule {
networkStateProvider: NetworkStateProvider,
localDataSource: AssignmentDetailsLocalDataSource,
networkDataSource: AssignmentDetailsNetworkDataSource,
- featureFlagProvider: FeatureFlagProvider
+ featureFlagProvider: FeatureFlagProvider,
+ reminderDao: ReminderDao
): AssignmentDetailsRepository {
- return AssignmentDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider)
+ return AssignmentDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider, reminderDao)
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt
index 5dc83c242f..d9cf249358 100644
--- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt
@@ -17,9 +17,14 @@
package com.instructure.student.features.assignments.details
+import android.app.AlarmManager
import android.app.Dialog
+import android.content.Context
+import android.content.Intent
import android.net.Uri
+import android.os.Build
import android.os.Bundle
+import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -27,10 +32,17 @@ import android.webkit.WebView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.viewModels
+import com.google.android.material.snackbar.Snackbar
import com.instructure.canvasapi2.CanvasRestAdapter
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.Assignment.SubmissionType
-import com.instructure.canvasapi2.utils.*
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.canvasapi2.models.RemoteFile
+import com.instructure.canvasapi2.utils.Analytics
+import com.instructure.canvasapi2.utils.AnalyticsEventConstants
+import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.pageview.PageView
import com.instructure.canvasapi2.utils.pageview.PageViewUrlParam
import com.instructure.interactions.bookmarks.Bookmarkable
@@ -41,7 +53,18 @@ import com.instructure.pandautils.analytics.SCREEN_VIEW_ASSIGNMENT_DETAILS
import com.instructure.pandautils.analytics.ScreenView
import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment
import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget
-import com.instructure.pandautils.utils.*
+import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.LongArg
+import com.instructure.pandautils.utils.ParcelableArg
+import com.instructure.pandautils.utils.PermissionUtils
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.pandautils.utils.makeBundle
+import com.instructure.pandautils.utils.orDefault
+import com.instructure.pandautils.utils.setVisible
+import com.instructure.pandautils.utils.setupAsBackButton
+import com.instructure.pandautils.utils.showThemed
+import com.instructure.pandautils.utils.toast
+import com.instructure.pandautils.utils.withArgs
import com.instructure.pandautils.views.CanvasWebView
import com.instructure.pandautils.views.RecordingMediaType
import com.instructure.student.R
@@ -49,7 +72,12 @@ import com.instructure.student.activity.BaseRouterActivity
import com.instructure.student.databinding.DialogSubmissionPickerBinding
import com.instructure.student.databinding.DialogSubmissionPickerMediaBinding
import com.instructure.student.databinding.FragmentAssignmentDetailsBinding
-import com.instructure.student.fragment.*
+import com.instructure.student.features.assignments.reminder.CustomReminderDialog
+import com.instructure.student.fragment.BasicQuizViewFragment
+import com.instructure.student.fragment.InternalWebviewFragment
+import com.instructure.student.fragment.LtiLaunchFragment
+import com.instructure.student.fragment.ParentFragment
+import com.instructure.student.fragment.StudioWebViewFragment
import com.instructure.student.mobius.assignmentDetails.getVideoUri
import com.instructure.student.mobius.assignmentDetails.launchAudio
import com.instructure.student.mobius.assignmentDetails.needsPermissions
@@ -134,6 +162,11 @@ class AssignmentDetailsFragment : ParentFragment(), Bookmarkable {
}
}
+ override fun onResume() {
+ super.onResume()
+ checkAlarmPermissionResult()
+ }
+
private fun handleAction(action: AssignmentDetailAction) {
val canvasContext = canvasContext as? CanvasContext ?: run {
toast(R.string.generalUnexpectedError)
@@ -194,6 +227,15 @@ class AssignmentDetailsFragment : ParentFragment(), Bookmarkable {
is AssignmentDetailAction.OnDiscussionHeaderAttachmentClicked -> {
showDiscussionAttachments(action.attachments)
}
+ is AssignmentDetailAction.ShowReminderDialog -> {
+ checkAlarmPermission()
+ }
+ is AssignmentDetailAction.ShowCustomReminderDialog -> {
+ showCustomReminderDialog()
+ }
+ is AssignmentDetailAction.ShowDeleteReminderConfirmationDialog -> {
+ showDeleteReminderConfirmationDialog(action.onConfirmed)
+ }
}
}
@@ -387,6 +429,80 @@ class AssignmentDetailsFragment : ParentFragment(), Bookmarkable {
)
}
+ private fun checkAlarmPermission() {
+ val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (alarmManager.canScheduleExactAlarms()) {
+ showCreateReminderDialog()
+ } else {
+ viewModel.checkingReminderPermission = true
+ startActivity(
+ Intent(
+ Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
+ Uri.parse("package:" + requireContext().packageName)
+ )
+ )
+ }
+ } else {
+ showCreateReminderDialog()
+ }
+ }
+
+ private fun checkAlarmPermissionResult() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && viewModel.checkingReminderPermission) {
+ if ((context?.getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()) {
+ showCreateReminderDialog()
+ } else {
+ Snackbar.make(requireView(), getString(R.string.reminderPermissionNotGrantedError), Snackbar.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ private fun showCreateReminderDialog() {
+ val choices = listOf(
+ ReminderChoice.Minute(5),
+ ReminderChoice.Minute(15),
+ ReminderChoice.Minute(30),
+ ReminderChoice.Hour(1),
+ ReminderChoice.Day(1),
+ ReminderChoice.Week(1),
+ ReminderChoice.Custom,
+ )
+
+ AlertDialog.Builder(requireContext())
+ .setTitle(R.string.reminderTitle)
+ .setNegativeButton(R.string.cancel, null)
+ .setSingleChoiceItems(
+ choices.map {
+ if (it is ReminderChoice.Custom) {
+ it.getText(resources)
+ } else {
+ getString(R.string.reminderBefore, it.getText(resources))
+ }
+ }.toTypedArray(), -1
+ ) { dialog, which ->
+ viewModel.onReminderSelected(choices[which])
+ dialog.dismiss()
+ }
+ .showThemed()
+ }
+
+ private fun showCustomReminderDialog() {
+ CustomReminderDialog.newInstance().show(childFragmentManager, null)
+ }
+
+ private fun showDeleteReminderConfirmationDialog(onConfirmed: () -> Unit) {
+ AlertDialog.Builder(requireContext())
+ .setTitle(R.string.deleteReminderTitle)
+ .setMessage(R.string.deleteReminderMessage)
+ .setNegativeButton(R.string.no, null)
+ .setPositiveButton(R.string.yes) { dialog, _ ->
+ onConfirmed()
+ dialog.dismiss()
+ }
+ .showThemed()
+ }
+
companion object {
fun makeRoute(course: CanvasContext, assignmentId: Long): Route {
val bundle = course.makeBundle { putLong(Const.ASSIGNMENT_ID, assignmentId) }
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsRepository.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsRepository.kt
index 0a7d26035d..475df251da 100644
--- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsRepository.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsRepository.kt
@@ -17,11 +17,14 @@
package com.instructure.student.features.assignments.details
+import androidx.lifecycle.LiveData
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.LTITool
import com.instructure.canvasapi2.models.Quiz
import com.instructure.pandautils.repository.Repository
+import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
+import com.instructure.pandautils.room.appdatabase.entities.ReminderEntity
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsDataSource
@@ -32,7 +35,8 @@ class AssignmentDetailsRepository(
localDataSource: AssignmentDetailsLocalDataSource,
networkDataSource: AssignmentDetailsNetworkDataSource,
networkStateProvider: NetworkStateProvider,
- featureFlagProvider: FeatureFlagProvider
+ featureFlagProvider: FeatureFlagProvider,
+ private val reminderDao: ReminderDao
) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) {
suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course {
@@ -54,4 +58,23 @@ class AssignmentDetailsRepository(
suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): LTITool? {
return dataSource().getLtiFromAuthenticationUrl(url, forceNetwork)
}
+
+ fun getRemindersByAssignmentIdLiveData(userId: Long, assignmentId: Long): LiveData> {
+ return reminderDao.findByAssignmentIdLiveData(userId, assignmentId)
+ }
+
+ suspend fun deleteReminderById(id: Long) {
+ reminderDao.deleteById(id)
+ }
+
+ suspend fun addReminder(userId: Long, assignment: Assignment, text: String, time: Long) = reminderDao.insert(
+ ReminderEntity(
+ userId = userId,
+ assignmentId = assignment.id,
+ htmlUrl = assignment.htmlUrl.orEmpty(),
+ name = assignment.name.orEmpty(),
+ text = text,
+ time = time
+ )
+ )
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewData.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewData.kt
index 5058b054a4..9327ac087a 100644
--- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewData.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewData.kt
@@ -1,13 +1,20 @@
package com.instructure.student.features.assignments.details
+import android.content.res.Resources
import android.text.Spanned
import androidx.annotation.ColorRes
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.canvasapi2.models.Quiz
+import com.instructure.canvasapi2.models.RemoteFile
import com.instructure.pandautils.features.assignmentdetails.AssignmentDetailsAttemptItemViewModel
import com.instructure.pandautils.utils.ThemedColor
+import com.instructure.student.R
import com.instructure.student.features.assignments.details.gradecellview.GradeCellViewData
+import com.instructure.student.features.assignments.details.itemviewmodels.ReminderItemViewModel
data class AssignmentDetailsViewData(
val courseColor: ThemedColor,
@@ -32,7 +39,9 @@ data class AssignmentDetailsViewData(
val discussionHeaderViewData: DiscussionHeaderViewData? = null,
val quizDetails: QuizViewViewData? = null,
val attemptsViewData: AttemptsViewData? = null,
- @Bindable var hasDraft: Boolean = false
+ @Bindable var hasDraft: Boolean = false,
+ val showReminders: Boolean = false,
+ @Bindable var reminders: List = emptyList()
) : BaseObservable() {
val firstAttemptOrNull = attempts.firstOrNull()
val noDescriptionVisible = description.isEmpty() && !fullLocked
@@ -51,6 +60,32 @@ data class DiscussionHeaderViewData(
val onAttachmentClicked: () -> Unit
)
+data class ReminderViewData(val id: Long, val text: String)
+
+sealed class ReminderChoice {
+ data class Minute(val quantity: Int) : ReminderChoice()
+ data class Hour(val quantity: Int) : ReminderChoice()
+ data class Day(val quantity: Int) : ReminderChoice()
+ data class Week(val quantity: Int) : ReminderChoice()
+ data object Custom : ReminderChoice()
+
+ fun getText(resources: Resources) = when (this) {
+ is Minute -> resources.getQuantityString(R.plurals.reminderMinute, quantity, quantity)
+ is Hour -> resources.getQuantityString(R.plurals.reminderHour, quantity, quantity)
+ is Day -> resources.getQuantityString(R.plurals.reminderDay, quantity, quantity)
+ is Week -> resources.getQuantityString(R.plurals.reminderWeek, quantity, quantity)
+ is Custom -> resources.getString(R.string.reminderCustom)
+ }
+
+ fun getTimeInMillis() = when (this) {
+ is Minute -> quantity * 60 * 1000L
+ is Hour -> quantity * 60 * 60 * 1000L
+ is Day -> quantity * 24 * 60 * 60 * 1000L
+ is Week -> quantity * 7 * 24 * 60 * 60 * 1000L
+ else -> 0
+ }
+}
+
sealed class AssignmentDetailAction {
data class ShowToast(val message: String) : AssignmentDetailAction()
data class NavigateToLtiScreen(val url: String) : AssignmentDetailAction()
@@ -76,4 +111,7 @@ sealed class AssignmentDetailAction {
data class ShowSubmitDialog(val assignment: Assignment, val studioLTITool: LTITool?) : AssignmentDetailAction()
data class NavigateToUploadStatusScreen(val submissionId: Long) : AssignmentDetailAction()
data class OnDiscussionHeaderAttachmentClicked(val attachments: List) : AssignmentDetailAction()
+ data object ShowReminderDialog : AssignmentDetailAction()
+ data object ShowCustomReminderDialog : AssignmentDetailAction()
+ data class ShowDeleteReminderConfirmationDialog(val onConfirmed: () -> Unit) : AssignmentDetailAction()
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewModel.kt
index 487d856402..00ea9846cc 100644
--- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewModel.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewModel.kt
@@ -22,6 +22,7 @@ import android.content.Context
import android.content.res.Resources
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -44,6 +45,7 @@ import com.instructure.pandautils.features.assignmentdetails.AssignmentDetailsAt
import com.instructure.pandautils.features.assignmentdetails.AssignmentDetailsAttemptViewData
import com.instructure.pandautils.mvvm.Event
import com.instructure.pandautils.mvvm.ViewState
+import com.instructure.pandautils.room.appdatabase.entities.ReminderEntity
import com.instructure.pandautils.utils.AssignmentUtils2
import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.Const
@@ -52,6 +54,8 @@ import com.instructure.pandautils.utils.orDefault
import com.instructure.student.R
import com.instructure.student.db.StudentDb
import com.instructure.student.features.assignments.details.gradecellview.GradeCellViewData
+import com.instructure.student.features.assignments.details.itemviewmodels.ReminderItemViewModel
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
import com.instructure.student.mobius.assignmentDetails.getFormattedAttemptDate
import com.instructure.student.mobius.assignmentDetails.uploadAudioRecording
import com.instructure.student.util.getStudioLTITool
@@ -73,7 +77,8 @@ class AssignmentDetailsViewModel @Inject constructor(
private val htmlContentFormatter: HtmlContentFormatter,
private val colorKeeper: ColorKeeper,
private val application: Application,
- apiPrefs: ApiPrefs,
+ private val apiPrefs: ApiPrefs,
+ private val alarmScheduler: AlarmScheduler,
database: StudentDb
) : ViewModel(), Query.Listener {
@@ -112,6 +117,19 @@ class AssignmentDetailsViewModel @Inject constructor(
private val submissionQuery = database.submissionQueries.getSubmissionsByAssignmentId(assignmentId, apiPrefs.user?.id.orDefault())
+ private val remindersObserver = Observer> {
+ _data.value?.reminders = mapReminders(it)
+ _data.value?.notifyPropertyChanged(BR.reminders)
+ }
+
+ private val remindersLiveData = assignmentDetailsRepository.getRemindersByAssignmentIdLiveData(
+ apiPrefs.user?.id.orDefault(), assignmentId
+ ).apply {
+ observeForever(remindersObserver)
+ }
+
+ var checkingReminderPermission = false
+
init {
markSubmissionAsRead()
submissionQuery.addListener(this)
@@ -119,6 +137,11 @@ class AssignmentDetailsViewModel @Inject constructor(
loadData()
}
+ override fun onCleared() {
+ super.onCleared()
+ remindersLiveData.removeObserver(remindersObserver)
+ }
+
override fun queryResultsChanged() {
viewModelScope.launch {
val submission = submissionQuery.executeAsList().lastOrNull()
@@ -244,7 +267,6 @@ class AssignmentDetailsViewModel @Inject constructor(
}
}
- @Suppress("DEPRECATION")
private suspend fun getViewData(assignment: Assignment, hasDraft: Boolean): AssignmentDetailsViewData {
val points = if (restrictQuantitativeData) {
""
@@ -461,7 +483,9 @@ class AssignmentDetailsViewModel @Inject constructor(
discussionHeaderViewData = discussionHeaderViewData,
quizDetails = quizViewViewData,
attemptsViewData = attemptsViewData,
- hasDraft = hasDraft
+ hasDraft = hasDraft,
+ showReminders = assignment.dueDate?.after(Date()).orDefault(),
+ reminders = mapReminders(remindersLiveData.value.orEmpty())
)
}
@@ -469,6 +493,21 @@ class AssignmentDetailsViewModel @Inject constructor(
_events.postValue(Event(action))
}
+ private fun mapReminders(reminders: List) = reminders.map {
+ ReminderItemViewModel(ReminderViewData(it.id, resources.getString(R.string.reminderBefore, it.text))) {
+ postAction(AssignmentDetailAction.ShowDeleteReminderConfirmationDialog {
+ deleteReminderById(it)
+ })
+ }
+ }
+
+ private fun deleteReminderById(id: Long) {
+ alarmScheduler.cancelAlarm(id)
+ viewModelScope.launch {
+ assignmentDetailsRepository.deleteReminderById(id)
+ }
+ }
+
fun refresh() {
_state.postValue(ViewState.Refresh)
loadData(true)
@@ -574,4 +613,55 @@ class AssignmentDetailsViewModel @Inject constructor(
fun showContent(viewState: ViewState?): Boolean {
return (viewState == ViewState.Success || viewState == ViewState.Refresh) && assignment != null
}
+
+ fun onAddReminderClicked() {
+ postAction(AssignmentDetailAction.ShowReminderDialog)
+ }
+
+ fun onReminderSelected(reminderChoice: ReminderChoice) {
+ if (reminderChoice == ReminderChoice.Custom) {
+ postAction(AssignmentDetailAction.ShowCustomReminderDialog)
+ } else {
+ setReminder(reminderChoice)
+ }
+ }
+
+ private fun setReminder(reminderChoice: ReminderChoice) {
+ val assignment = assignment ?: return
+ val alarmTimeInMillis = getAlarmTimeInMillis(reminderChoice) ?: return
+ val reminderText = reminderChoice.getText(resources)
+
+ if (alarmTimeInMillis < System.currentTimeMillis()) {
+ postAction(AssignmentDetailAction.ShowToast(resources.getString(R.string.reminderInPast)))
+ return
+ }
+
+ if (remindersLiveData.value?.any { it.time == alarmTimeInMillis }.orDefault()) {
+ postAction(AssignmentDetailAction.ShowToast(resources.getString(R.string.reminderAlreadySet)))
+ return
+ }
+
+ viewModelScope.launch {
+ val reminderId = assignmentDetailsRepository.addReminder(
+ apiPrefs.user?.id.orDefault(),
+ assignment,
+ reminderText,
+ alarmTimeInMillis
+ )
+
+ alarmScheduler.scheduleAlarm(
+ assignment.id,
+ assignment.htmlUrl.orEmpty(),
+ assignment.name.orEmpty(),
+ reminderText,
+ alarmTimeInMillis,
+ reminderId
+ )
+ }
+ }
+
+ private fun getAlarmTimeInMillis(reminderChoice: ReminderChoice): Long? {
+ val dueDate = assignment?.dueDate?.time ?: return null
+ return dueDate - reminderChoice.getTimeInMillis()
+ }
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/itemviewmodels/ReminderItemViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/itemviewmodels/ReminderItemViewModel.kt
new file mode 100644
index 0000000000..bbd443a22c
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/itemviewmodels/ReminderItemViewModel.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 - 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.student.features.assignments.details.itemviewmodels
+
+import com.instructure.pandautils.mvvm.ItemViewModel
+import com.instructure.student.R
+import com.instructure.student.features.assignments.details.ReminderViewData
+
+class ReminderItemViewModel(
+ val data: ReminderViewData,
+ val onRemoveClick: (Long) -> Unit
+) : ItemViewModel {
+ override val layoutId: Int
+ get() = R.layout.view_reminder
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/reminder/AlarmScheduler.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/reminder/AlarmScheduler.kt
new file mode 100644
index 0000000000..1243b77cc5
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/reminder/AlarmScheduler.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 - 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.student.features.assignments.reminder
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
+import com.instructure.student.receivers.AlarmReceiver
+
+class AlarmScheduler(private val context: Context, private val reminderDao: ReminderDao, private val apiPrefs: ApiPrefs) {
+
+ private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+ fun scheduleAlarm(assignmentId: Long, assignmentPath: String, assignmentName: String, dueIn: String, timeInMillis: Long, reminderId: Long) {
+ val intent = Intent(context, AlarmReceiver::class.java)
+ intent.putExtra(AlarmReceiver.ASSIGNMENT_ID, assignmentId)
+ intent.putExtra(AlarmReceiver.ASSIGNMENT_PATH, assignmentPath)
+ intent.putExtra(AlarmReceiver.ASSIGNMENT_NAME, assignmentName)
+ intent.putExtra(AlarmReceiver.DUE_IN, dueIn)
+
+ val pendingIntent = PendingIntent.getBroadcast(
+ context,
+ reminderId.toInt(),
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) return
+
+ alarmManager.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent)
+ }
+
+ suspend fun scheduleAllAlarmsForCurrentUser() {
+ val reminders = reminderDao.findByUserId(apiPrefs.user?.id ?: return)
+ reminders.forEach {
+ scheduleAlarm(it.assignmentId, it.htmlUrl, it.name, it.text, it.time, it.id)
+ }
+ }
+
+ fun cancelAlarm(reminderId: Long) {
+ val intent = Intent(context, AlarmReceiver::class.java)
+
+ val pendingIntent = PendingIntent.getBroadcast(
+ context,
+ reminderId.toInt(),
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ alarmManager.cancel(pendingIntent)
+ }
+
+ suspend fun cancelAllAlarmsForCurrentUser() {
+ val reminders = reminderDao.findByUserId(apiPrefs.user?.id ?: return)
+ reminders.forEach {
+ cancelAlarm(it.id)
+ }
+ }
+}
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/reminder/CustomReminderDialog.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/reminder/CustomReminderDialog.kt
new file mode 100644
index 0000000000..311829ba1f
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/reminder/CustomReminderDialog.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 - 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.student.features.assignments.reminder
+
+import android.app.Dialog
+import android.content.res.ColorStateList
+import android.os.Bundle
+import android.widget.Button
+import androidx.appcompat.app.AlertDialog
+import androidx.core.widget.doAfterTextChanged
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.viewModels
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.student.R
+import com.instructure.student.databinding.DialogCustomReminderBinding
+import com.instructure.student.features.assignments.details.AssignmentDetailsViewModel
+import com.instructure.student.features.assignments.details.ReminderChoice
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class CustomReminderDialog : DialogFragment() {
+
+ private lateinit var binding: DialogCustomReminderBinding
+ private val parentViewModel: AssignmentDetailsViewModel by viewModels(ownerProducer = {
+ requireParentFragment()
+ })
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ binding = DialogCustomReminderBinding.inflate(layoutInflater, null, false)
+
+ return AlertDialog.Builder(requireContext())
+ .setView(binding.root)
+ .setTitle(R.string.customReminderTitle)
+ .setPositiveButton(R.string.done) { _, _ ->
+ val quantity = binding.quantity.text.toString().toIntOrNull() ?: return@setPositiveButton
+ when (binding.choices.checkedRadioButtonId) {
+ R.id.minutes -> parentViewModel.onReminderSelected(ReminderChoice.Minute(quantity))
+ R.id.hours -> parentViewModel.onReminderSelected(ReminderChoice.Hour(quantity))
+ R.id.days -> parentViewModel.onReminderSelected(ReminderChoice.Day(quantity))
+ R.id.weeks -> parentViewModel.onReminderSelected(ReminderChoice.Week(quantity))
+ }
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .create().apply {
+ setOnShowListener {
+ getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ThemePrefs.textButtonColor)
+ setupPositiveButton(getButton(AlertDialog.BUTTON_POSITIVE))
+ }
+ }
+ }
+
+ private fun setupPositiveButton(button: Button) {
+ button.isEnabled = false
+ button.setTextColor(
+ ColorStateList(
+ arrayOf(intArrayOf(-android.R.attr.state_enabled), intArrayOf()),
+ intArrayOf(requireContext().getColor(R.color.textDark), ThemePrefs.textButtonColor)
+ )
+ )
+ binding.choices.setOnCheckedChangeListener { _, _ -> updateButtonState(button) }
+ binding.quantity.doAfterTextChanged { updateButtonState(button) }
+ }
+
+ private fun updateButtonState(button: Button) {
+ button.isEnabled = binding.choices.checkedRadioButtonId != -1 && binding.quantity.text.isNotEmpty()
+ }
+
+ companion object {
+ fun newInstance() = CustomReminderDialog()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/coursebrowser/CourseBrowserFragment.kt b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/CourseBrowserFragment.kt
index 68c048d916..79ca2d53e8 100644
--- a/apps/student/src/main/java/com/instructure/student/features/coursebrowser/CourseBrowserFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/CourseBrowserFragment.kt
@@ -213,14 +213,20 @@ class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnO
// Load Pages List
if (tabs.any { it.tabId == Tab.PAGES_ID }) {
// Do not load the pages list if the tab is hidden or locked.
- RouteMatcher.route(requireActivity(), TabHelper.getRouteByTabId(tab, canvasContext))
+ val route = TabHelper.getRouteByTabId(tab, canvasContext)
+ route?.arguments = route?.arguments?.apply {
+ putString(PageDetailsFragment.PAGE_NAME, homePageTitle)
+ } ?: Bundle()
+ RouteMatcher.route(requireActivity(), route)
}
// If the home tab is a Page and we clicked it lets route directly there.
- RouteMatcher.route(
- requireActivity(),
- PageDetailsFragment.makeRoute(canvasContext, Page.FRONT_PAGE_NAME)
- .apply { ignoreDebounce = true })
+ val route = PageDetailsFragment.makeFrontPageRoute(canvasContext)
+ .apply { ignoreDebounce = true }
+ route.arguments = route.arguments.apply {
+ putString(PageDetailsFragment.PAGE_NAME, homePageTitle)
+ }
+ RouteMatcher.route(requireActivity(), route)
} else {
val route = TabHelper.getRouteByTabId(tab, canvasContext)?.apply { ignoreDebounce = true }
RouteMatcher.route(requireActivity(), route)
diff --git a/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt b/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt
index ef195aaa84..1884f27bfd 100644
--- a/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt
@@ -27,11 +27,13 @@ import com.instructure.pandautils.services.PushNotificationRegistrationWorker
import com.instructure.student.R
import com.instructure.student.activity.InternalWebViewActivity
import com.instructure.student.activity.NavigationActivity
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
import com.instructure.student.tasks.StudentLogoutTask
class StudentAcceptableUsePolicyRouter(
private val activity: FragmentActivity,
- private val databaseProvider: DatabaseProvider
+ private val databaseProvider: DatabaseProvider,
+ private val alarmScheduler: AlarmScheduler
) : AcceptableUsePolicyRouter {
override fun openPolicy(content: String) {
@@ -54,6 +56,6 @@ class StudentAcceptableUsePolicyRouter(
}
override fun logout() {
- StudentLogoutTask(LogoutTask.Type.LOGOUT, databaseProvider = databaseProvider).execute()
+ StudentLogoutTask(LogoutTask.Type.LOGOUT, databaseProvider = databaseProvider, alarmScheduler = alarmScheduler).execute()
}
}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt b/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt
index 8c562f1760..a11e91a18a 100644
--- a/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt
@@ -25,16 +25,18 @@ import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.pandautils.room.offline.DatabaseProvider
import com.instructure.pandautils.services.PushNotificationRegistrationWorker
import com.instructure.student.activity.NavigationActivity
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
import com.instructure.student.tasks.StudentLogoutTask
class StudentLoginNavigation(
private val activity: FragmentActivity,
- private val databaseProvider: DatabaseProvider
+ private val databaseProvider: DatabaseProvider,
+ private val alarmScheduler: AlarmScheduler
) : LoginNavigation(activity) {
override val checkElementary: Boolean = true
override fun logout() {
- StudentLogoutTask(LogoutTask.Type.LOGOUT, databaseProvider = databaseProvider).execute()
+ StudentLogoutTask(LogoutTask.Type.LOGOUT, databaseProvider = databaseProvider, alarmScheduler = alarmScheduler).execute()
}
override fun initMainActivityIntent(): Intent {
diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt
index ae55c2f3ef..fba5a92eac 100644
--- a/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt
@@ -51,7 +51,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import org.greenrobot.eventbus.Subscribe
import java.util.*
-import java.util.regex.Pattern
+import java.util.regex.*
import javax.inject.Inject
@ScreenView(SCREEN_VIEW_PAGE_DETAILS)
@@ -67,6 +67,7 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable {
private var page: Page by ParcelableArg(default = Page(), key = PAGE)
private var pageUrl: String? by NullableStringArg(key = PAGE_URL)
private var navigatedFromModules: Boolean by BooleanArg(key = NAVIGATED_FROM_MODULES)
+ private var frontPage: Boolean by BooleanArg(key = FRONT_PAGE)
// Flag for the webview client to know whether or not we should clear the history
private var isUpdated = false
@@ -152,11 +153,11 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable {
} else {
loadFailedPageInfo(null)
}
- } else if (pageName == null || pageName == Page.FRONT_PAGE_NAME) fetchFontPage()
+ } else if (frontPage) fetchFrontPage()
else fetchPageDetails()
}
- private fun fetchFontPage() {
+ private fun fetchFrontPage() {
lifecycleScope.tryLaunch {
val result = repository.getFrontPage(canvasContext, true)
result.onSuccess {
@@ -331,6 +332,7 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable {
const val PAGE = "pageDetails"
const val PAGE_URL = "pageUrl"
const val NAVIGATED_FROM_MODULES = "navigated_from_modules"
+ private const val FRONT_PAGE = "frontPage"
fun newInstance(route: Route): PageDetailsFragment? {
return if (validRoute(route)) PageDetailsFragment().apply {
@@ -349,9 +351,9 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable {
route.paramsHash.containsKey(RouterParams.PAGE_ID))
}
- fun makeRoute(canvasContext: CanvasContext, pageName: String?): Route {
+ fun makeFrontPageRoute(canvasContext: CanvasContext): Route {
return Route(null, PageDetailsFragment::class.java, canvasContext, canvasContext.makeBundle(Bundle().apply {
- if (pageName != null) putString(PAGE_NAME, pageName)
+ putBoolean(FRONT_PAGE, true)
}))
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListFragment.kt
index 740d0c4816..cb47a42ce2 100644
--- a/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListFragment.kt
@@ -129,9 +129,8 @@ class PageListFragment : ParentFragment(), Bookmarkable {
super.onActivityCreated(savedInstanceState)
if (isShowFrontPage) {
- val route = PageDetailsFragment.makeRoute(
- canvasContext,
- Page.FRONT_PAGE_NAME
+ val route = PageDetailsFragment.makeFrontPageRoute(
+ canvasContext
).apply { ignoreDebounce = true}
RouteMatcher.route(requireActivity(), route)
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListRecyclerAdapter.kt
index a74128b780..01cf25e3f6 100644
--- a/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListRecyclerAdapter.kt
@@ -41,15 +41,24 @@ import kotlinx.coroutines.CoroutineScope
import java.util.Locale
class PeopleListRecyclerAdapter(
- context: Context,
- private val lifecycleScope: CoroutineScope,
- private val repository: PeopleListRepository,
- private val canvasContext: CanvasContext,
- private val adapterToFragmentCallback: AdapterToFragmentCallback
-) : ExpandableRecyclerAdapter(context, EnrollmentType::class.java, User::class.java) {
+ context: Context,
+ private val lifecycleScope: CoroutineScope,
+ private val repository: PeopleListRepository,
+ private val canvasContext: CanvasContext,
+ private val adapterToFragmentCallback: AdapterToFragmentCallback
+) : ExpandableRecyclerAdapter(
+ context,
+ EnrollmentType::class.java,
+ User::class.java
+) {
private val mCourseColor = canvasContext.backgroundColor
- private val mEnrollmentPriority = mapOf( EnrollmentType.Teacher to 4, EnrollmentType.Ta to 3, EnrollmentType.Student to 2, EnrollmentType.Observer to 1)
+ private val mEnrollmentPriority = mapOf(
+ EnrollmentType.Teacher to 4,
+ EnrollmentType.Ta to 3,
+ EnrollmentType.Student to 2,
+ EnrollmentType.Observer to 1
+ )
init {
isExpandedByDefault = true
@@ -58,16 +67,18 @@ class PeopleListRecyclerAdapter(
override fun loadFirstPage() {
lifecycleScope.tryLaunch {
- var canvasContext = canvasContext
-
- // If the canvasContext is a group, and has a course we want to add the Teachers and TAs from that course to the peoples list
- if (CanvasContext.Type.isGroup(this@PeopleListRecyclerAdapter.canvasContext) && (this@PeopleListRecyclerAdapter.canvasContext as Group).courseId > 0) {
- // We build a generic CanvasContext with type set to COURSE and give it the CourseId from the group, so that it wil use the course API not the group API
- canvasContext = CanvasContext.getGenericContext(CanvasContext.Type.COURSE, this@PeopleListRecyclerAdapter.canvasContext.courseId, "")
- }
-
- val teachers = repository.loadTeachers(canvasContext, isRefresh)
- val tas = repository.loadTAs(canvasContext, isRefresh)
+ val teacherContext =
+ if (CanvasContext.Type.isGroup(this@PeopleListRecyclerAdapter.canvasContext) && (this@PeopleListRecyclerAdapter.canvasContext as Group).courseId > 0) {
+ // We build a generic CanvasContext with type set to COURSE and give it the CourseId from the group, so that it wil use the course API not the group API
+ CanvasContext.getGenericContext(
+ CanvasContext.Type.COURSE,
+ this@PeopleListRecyclerAdapter.canvasContext.courseId,
+ ""
+ )
+ } else canvasContext
+
+ val teachers = repository.loadTeachers(teacherContext, isRefresh)
+ val tas = repository.loadTAs(teacherContext, isRefresh)
val peopleFirstPage = repository.loadFirstPagePeople(canvasContext, isRefresh)
val result = teachers.dataOrThrow + tas.dataOrThrow + peopleFirstPage.dataOrThrow
@@ -102,38 +113,59 @@ class PeopleListRecyclerAdapter(
private fun populateAdapter(result: List) {
val (enrolled, unEnrolled) = result.partition { it.enrollments.isNotEmpty() }
enrolled
- .groupBy {
- it.enrollments.sortedByDescending { enrollment -> mEnrollmentPriority[enrollment.type] }[0].type
- }
- .forEach { (type, users) -> addOrUpdateAllItems(type!!, users) }
+ .groupBy {
+ it.enrollments.sortedByDescending { enrollment -> mEnrollmentPriority[enrollment.type] }[0].type
+ }
+ .forEach { (type, users) -> addOrUpdateAllItems(type!!, users) }
if (CanvasContext.Type.isGroup(canvasContext)) addOrUpdateAllItems(EnrollmentType.NoEnrollment, unEnrolled)
notifyDataSetChanged()
adapterToFragmentCallback.onRefreshFinished()
}
override fun createViewHolder(v: View, viewType: Int): RecyclerView.ViewHolder =
- if (viewType == Types.TYPE_HEADER) PeopleHeaderViewHolder(v) else PeopleViewHolder(v)
+ if (viewType == Types.TYPE_HEADER) PeopleHeaderViewHolder(v) else PeopleViewHolder(v)
override fun itemLayoutResId(viewType: Int): Int =
- if (viewType == Types.TYPE_HEADER) PeopleHeaderViewHolder.HOLDER_RES_ID else PeopleViewHolder.HOLDER_RES_ID
+ if (viewType == Types.TYPE_HEADER) PeopleHeaderViewHolder.HOLDER_RES_ID else PeopleViewHolder.HOLDER_RES_ID
override fun contextReady() = Unit
override fun onBindChildHolder(holder: RecyclerView.ViewHolder, peopleGroupType: EnrollmentType, user: User) {
val groupItemCount = getGroupItemCount(peopleGroupType)
val itemPosition = storedIndexOfItem(peopleGroupType, user)
- (holder as PeopleViewHolder).bind(user, adapterToFragmentCallback, mCourseColor, itemPosition == 0, itemPosition == groupItemCount - 1)
+ (holder as PeopleViewHolder).bind(
+ user,
+ adapterToFragmentCallback,
+ mCourseColor,
+ itemPosition == 0,
+ itemPosition == groupItemCount - 1
+ )
}
- override fun onBindHeaderHolder(holder: RecyclerView.ViewHolder, enrollmentType: EnrollmentType, isExpanded: Boolean) {
- (holder as PeopleHeaderViewHolder).bind(enrollmentType, getHeaderTitle(enrollmentType), isExpanded, viewHolderHeaderClicked)
+ override fun onBindHeaderHolder(
+ holder: RecyclerView.ViewHolder,
+ enrollmentType: EnrollmentType,
+ isExpanded: Boolean
+ ) {
+ (holder as PeopleHeaderViewHolder).bind(
+ enrollmentType,
+ getHeaderTitle(enrollmentType),
+ isExpanded,
+ viewHolderHeaderClicked
+ )
}
override fun createGroupCallback(): GroupSortedList.GroupComparatorCallback {
return object : GroupSortedList.GroupComparatorCallback {
- override fun compare(o1: EnrollmentType, o2: EnrollmentType) = getHeaderTitle(o2).compareTo(getHeaderTitle(o1))
- override fun areContentsTheSame(oldGroup: EnrollmentType, newGroup: EnrollmentType) = getHeaderTitle(oldGroup) == getHeaderTitle(newGroup)
- override fun areItemsTheSame(group1: EnrollmentType, group2: EnrollmentType) = getHeaderTitle(group1) == getHeaderTitle(group2)
+ override fun compare(o1: EnrollmentType, o2: EnrollmentType) =
+ getHeaderTitle(o2).compareTo(getHeaderTitle(o1))
+
+ override fun areContentsTheSame(oldGroup: EnrollmentType, newGroup: EnrollmentType) =
+ getHeaderTitle(oldGroup) == getHeaderTitle(newGroup)
+
+ override fun areItemsTheSame(group1: EnrollmentType, group2: EnrollmentType) =
+ getHeaderTitle(group1) == getHeaderTitle(group2)
+
override fun getUniqueGroupId(group: EnrollmentType) = getHeaderTitle(group).hashCode().toLong()
override fun getGroupType(group: EnrollmentType) = Types.TYPE_HEADER
}
@@ -141,7 +173,11 @@ class PeopleListRecyclerAdapter(
override fun createItemCallback(): GroupSortedList.ItemComparatorCallback {
return object : GroupSortedList.ItemComparatorCallback {
- override fun compare(group: EnrollmentType, o1: User, o2: User) = NaturalOrderComparator.compare(o1.sortableName?.lowercase(Locale.getDefault()).orEmpty(), o2.sortableName?.lowercase(Locale.getDefault()).orEmpty())
+ override fun compare(group: EnrollmentType, o1: User, o2: User) = NaturalOrderComparator.compare(
+ o1.sortableName?.lowercase(Locale.getDefault()).orEmpty(),
+ o2.sortableName?.lowercase(Locale.getDefault()).orEmpty()
+ )
+
override fun areContentsTheSame(oldItem: User, newItem: User) = oldItem.sortableName == newItem.sortableName
override fun areItemsTheSame(item1: User, item2: User) = item1.id == item2.id
override fun getUniqueItemId(item: User) = item.id
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt
index b599ab9883..2ba2396090 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt
@@ -25,6 +25,8 @@ import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
@@ -36,6 +38,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo.State
import androidx.work.WorkManager
import androidx.work.WorkQuery
+import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.instructure.canvasapi2.managers.CourseNicknameManager
import com.instructure.canvasapi2.managers.UserManager
import com.instructure.canvasapi2.models.*
@@ -99,6 +102,9 @@ class DashboardFragment : ParentFragment() {
@Inject
lateinit var workManager: WorkManager
+ @Inject
+ lateinit var firebaseCrashlytics: FirebaseCrashlytics
+
private val binding by viewBinding(FragmentCourseGridBinding::bind)
private lateinit var recyclerBinding: CourseGridRecyclerRefreshLayoutBinding
@@ -357,6 +363,10 @@ class DashboardFragment : ParentFragment() {
addItemTouchHelperForCardReorder()
}
+ fun cancelCardDrag() {
+ recyclerBinding.listView.onTouchEvent(MotionEvent.obtain(0L, 0L, ACTION_CANCEL, 0f, 0f, 0))
+ }
+
private fun addItemTouchHelperForCardReorder() {
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.START or ItemTouchHelper.END or ItemTouchHelper.DOWN or ItemTouchHelper.UP,
@@ -414,10 +424,17 @@ class DashboardFragment : ParentFragment() {
) {
val finishingPosition = viewHolder.bindingAdapterPosition
+ if (finishingPosition == RecyclerView.NO_POSITION) {
+ itemToMove = null
+ firebaseCrashlytics.recordException(Throwable("Failed to reorder dashboard. finishingPosition == RecyclerView.NO_POSITION"))
+ toast(R.string.failedToUpdateDashboardOrder)
+ return
+ }
+
itemToMove?.let {
recyclerAdapter?.moveItems(DashboardRecyclerAdapter.ItemType.COURSE_HEADER, it, finishingPosition - 1)
recyclerAdapter?.notifyDataSetChanged()
- itemToMove == null
+ itemToMove = null
}
val courseItems = recyclerAdapter?.getItems(DashboardRecyclerAdapter.ItemType.COURSE_HEADER)
diff --git a/apps/student/src/main/java/com/instructure/student/navigation/DefaultNavigationBehavior.kt b/apps/student/src/main/java/com/instructure/student/navigation/DefaultNavigationBehavior.kt
index 49bb171205..558b7761ba 100644
--- a/apps/student/src/main/java/com/instructure/student/navigation/DefaultNavigationBehavior.kt
+++ b/apps/student/src/main/java/com/instructure/student/navigation/DefaultNavigationBehavior.kt
@@ -20,7 +20,7 @@ import androidx.fragment.app.Fragment
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.interactions.router.Route
-import com.instructure.pandautils.utils.FontFamily
+import com.instructure.pandautils.utils.CanvasFont
import com.instructure.student.R
import com.instructure.student.fragment.CalendarFragment
import com.instructure.student.fragment.DashboardFragment
@@ -46,8 +46,8 @@ class DefaultNavigationBehavior(private val apiPrefs: ApiPrefs) : NavigationBeha
override val visibleAccountMenuItems: Set = setOf(AccountMenuItem.HELP, AccountMenuItem.CHANGE_USER, AccountMenuItem.LOGOUT)
- override val fontFamily: FontFamily
- get() = FontFamily.REGULAR
+ override val canvasFont: CanvasFont
+ get() = CanvasFont.REGULAR
override val bottomBarMenu: Int = R.menu.bottom_bar_menu
diff --git a/apps/student/src/main/java/com/instructure/student/navigation/ElementaryNavigationBehavior.kt b/apps/student/src/main/java/com/instructure/student/navigation/ElementaryNavigationBehavior.kt
index 37bbbfa2ea..9c36505bea 100644
--- a/apps/student/src/main/java/com/instructure/student/navigation/ElementaryNavigationBehavior.kt
+++ b/apps/student/src/main/java/com/instructure/student/navigation/ElementaryNavigationBehavior.kt
@@ -20,7 +20,7 @@ import androidx.fragment.app.Fragment
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.interactions.router.Route
-import com.instructure.pandautils.utils.FontFamily
+import com.instructure.pandautils.utils.CanvasFont
import com.instructure.student.R
import com.instructure.student.fragment.CalendarFragment
import com.instructure.student.fragment.NotificationListFragment
@@ -46,8 +46,8 @@ class ElementaryNavigationBehavior(private val apiPrefs: ApiPrefs) : NavigationB
override val visibleAccountMenuItems: Set = setOf(AccountMenuItem.HELP, AccountMenuItem.CHANGE_USER, AccountMenuItem.LOGOUT)
- override val fontFamily: FontFamily
- get() = FontFamily.K5
+ override val canvasFont: CanvasFont
+ get() = CanvasFont.K5
override val bottomBarMenu: Int = R.menu.bottom_bar_menu_elementary
diff --git a/apps/student/src/main/java/com/instructure/student/navigation/NavigationBehavior.kt b/apps/student/src/main/java/com/instructure/student/navigation/NavigationBehavior.kt
index e650131e0b..7ac1999403 100644
--- a/apps/student/src/main/java/com/instructure/student/navigation/NavigationBehavior.kt
+++ b/apps/student/src/main/java/com/instructure/student/navigation/NavigationBehavior.kt
@@ -22,7 +22,7 @@ import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.interactions.router.Route
import com.instructure.pandautils.features.inbox.list.InboxFragment
-import com.instructure.pandautils.utils.FontFamily
+import com.instructure.pandautils.utils.CanvasFont
import com.instructure.student.activity.NothingToSeeHereFragment
import com.instructure.student.fragment.ParentFragment
@@ -39,7 +39,7 @@ interface NavigationBehavior {
val visibleAccountMenuItems: Set
- val fontFamily: FontFamily
+ val canvasFont: CanvasFont
@get:MenuRes
val bottomBarMenu: Int
diff --git a/apps/student/src/main/java/com/instructure/student/receivers/AlarmReceiver.kt b/apps/student/src/main/java/com/instructure/student/receivers/AlarmReceiver.kt
new file mode 100644
index 0000000000..0f02fe27f8
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/receivers/AlarmReceiver.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 - 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.student.receivers
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.NotificationCompat
+import com.instructure.pandautils.models.PushNotification
+import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
+import com.instructure.pandautils.utils.Const
+import com.instructure.student.R
+import com.instructure.student.activity.NavigationActivity
+import com.instructure.student.util.goAsync
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class AlarmReceiver : BroadcastReceiver() {
+
+ @Inject
+ lateinit var reminderDao: ReminderDao
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (context != null && intent != null) {
+ val assignmentId = intent.getLongExtra(ASSIGNMENT_ID, 0L)
+ val assignmentPath = intent.getStringExtra(ASSIGNMENT_PATH) ?: return
+ val assignmentName = intent.getStringExtra(ASSIGNMENT_NAME) ?: return
+ val dueIn = intent.getStringExtra(DUE_IN) ?: return
+
+ createNotificationChannel(context)
+ showNotification(context, assignmentId, assignmentPath, assignmentName, dueIn)
+ goAsync {
+ reminderDao.deletePastReminders(System.currentTimeMillis())
+ }
+ }
+ }
+
+ private fun showNotification(context: Context, assignmentId: Long, assignmentPath: String, assignmentName: String, dueIn: String) {
+ val intent = Intent(context, NavigationActivity.startActivityClass).apply {
+ putExtra(Const.LOCAL_NOTIFICATION, true)
+ putExtra(PushNotification.HTML_URL, assignmentPath)
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ context, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val builder = NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notification_canvas_logo)
+ .setContentTitle(context.getString(R.string.reminderNotificationTitle))
+ .setContentText(context.getString(R.string.reminderNotificationDescription, dueIn, assignmentName))
+ .setAutoCancel(true)
+ .setContentIntent(pendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.notify(assignmentId.toInt(), builder.build())
+ }
+
+ private fun createNotificationChannel(context: Context) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ context.getString(R.string.reminderNotificationChannelName),
+ NotificationManager.IMPORTANCE_DEFAULT
+ ).apply {
+ description = context.getString(R.string.reminderNotificationChannelDescription)
+ }
+
+ val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ companion object {
+ private const val CHANNEL_ID = "REMINDERS_CHANNEL_ID"
+ const val ASSIGNMENT_ID = "ASSIGNMENT_ID"
+ const val ASSIGNMENT_PATH = "ASSIGNMENT_PATH"
+ const val ASSIGNMENT_NAME = "ASSIGNMENT_NAME"
+ const val DUE_IN = "DUE_IN"
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/receivers/InitializeReceiver.kt b/apps/student/src/main/java/com/instructure/student/receivers/InitializeReceiver.kt
index 090bf55a72..90560f5f20 100644
--- a/apps/student/src/main/java/com/instructure/student/receivers/InitializeReceiver.kt
+++ b/apps/student/src/main/java/com/instructure/student/receivers/InitializeReceiver.kt
@@ -20,18 +20,29 @@ package com.instructure.student.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import com.instructure.pandautils.receivers.PushExternalReceiver
import com.instructure.student.R
import com.instructure.student.activity.NavigationActivity
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
+import com.instructure.student.util.goAsync
import com.instructure.student.widget.WidgetUpdater
-import com.instructure.pandautils.receivers.PushExternalReceiver
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+@AndroidEntryPoint
class InitializeReceiver : BroadcastReceiver() {
+ @Inject
+ lateinit var alarmScheduler: AlarmScheduler
+
override fun onReceive(context: Context, intent: Intent) {
if(Intent.ACTION_BOOT_COMPLETED == intent.action || Intent.ACTION_MY_PACKAGE_REPLACED == intent.action) {
//Restores stored push notifications upon boot
PushExternalReceiver.postStoredNotifications(context, context.getString(R.string.student_app_name), NavigationActivity.startActivityClass, R.color.login_studentAppTheme)
WidgetUpdater.updateWidgets()
+ goAsync {
+ alarmScheduler.scheduleAllAlarmsForCurrentUser()
+ }
}
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt
index 59360f590b..e695af1e4f 100644
--- a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt
+++ b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt
@@ -29,6 +29,7 @@ import com.instructure.pandautils.features.offline.sync.OfflineSyncWorker
import com.instructure.pandautils.room.offline.DatabaseProvider
import com.instructure.pandautils.typeface.TypefaceBehavior
import com.instructure.student.activity.LoginActivity
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
import com.instructure.student.flutterChannels.FlutterComm
import com.instructure.student.util.StudentPrefs
import com.instructure.student.widget.WidgetUpdater
@@ -39,7 +40,8 @@ class StudentLogoutTask(
uri: Uri? = null,
canvasForElementaryFeatureFlag: Boolean = false,
typefaceBehavior: TypefaceBehavior? = null,
- private val databaseProvider: DatabaseProvider? = null
+ private val databaseProvider: DatabaseProvider? = null,
+ private val alarmScheduler: AlarmScheduler? = null
) : LogoutTask(type, uri, canvasForElementaryFeatureFlag, typefaceBehavior) {
override fun onCleanup() {
@@ -82,4 +84,8 @@ class StudentLogoutTask(
cancelAllWorkByTag(OfflineSyncWorker.ONE_TIME_TAG)
}
}
+
+ override suspend fun cancelAlarms() {
+ alarmScheduler?.cancelAllAlarmsForCurrentUser()
+ }
}
diff --git a/apps/student/src/main/java/com/instructure/student/util/AppManager.kt b/apps/student/src/main/java/com/instructure/student/util/AppManager.kt
index 20a7c1fa77..c43f7fb665 100644
--- a/apps/student/src/main/java/com/instructure/student/util/AppManager.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/AppManager.kt
@@ -23,6 +23,7 @@ import com.instructure.canvasapi2.utils.MasqueradeHelper
import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.pandautils.room.offline.DatabaseProvider
import com.instructure.pandautils.typeface.TypefaceBehavior
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
import com.instructure.student.tasks.StudentLogoutTask
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
@@ -39,15 +40,28 @@ class AppManager : BaseAppManager() {
@Inject
lateinit var databaseProvider: DatabaseProvider
+ @Inject
+ lateinit var alarmScheduler: AlarmScheduler
+
override fun onCreate() {
super.onCreate()
MasqueradeHelper.masqueradeLogoutTask = Runnable {
- StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior, databaseProvider = databaseProvider).execute()
+ StudentLogoutTask(
+ LogoutTask.Type.LOGOUT,
+ typefaceBehavior = typefaceBehavior,
+ databaseProvider = databaseProvider,
+ alarmScheduler = alarmScheduler
+ ).execute()
}
}
override fun performLogoutOnAuthError() {
- StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior, databaseProvider = databaseProvider).execute()
+ StudentLogoutTask(
+ LogoutTask.Type.LOGOUT,
+ typefaceBehavior = typefaceBehavior,
+ databaseProvider = databaseProvider,
+ alarmScheduler = alarmScheduler
+ ).execute()
}
override fun getWorkManagerFactory(): WorkerFactory = workerFactory
diff --git a/apps/student/src/main/java/com/instructure/student/util/Extensions.kt b/apps/student/src/main/java/com/instructure/student/util/Extensions.kt
index 4190da75d2..110f6fd2c9 100644
--- a/apps/student/src/main/java/com/instructure/student/util/Extensions.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/Extensions.kt
@@ -15,6 +15,7 @@
*/
package com.instructure.student.util
+import android.content.BroadcastReceiver
import android.content.Context
import com.instructure.canvasapi2.managers.ExternalToolManager
import com.instructure.canvasapi2.models.Assignment
@@ -24,8 +25,14 @@ import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.DataResult
import com.instructure.pandautils.utils.getShortMonthAndDay
import com.instructure.pandautils.utils.getTime
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import org.threeten.bp.OffsetDateTime
-import java.util.*
+import java.util.Locale
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
suspend fun Long.isStudioEnabled(): Boolean {
val context = CanvasContext.getGenericContext(CanvasContext.Type.COURSE, this)
@@ -52,3 +59,18 @@ fun String.toDueAtString(context: Context): String {
val dueDateTime = OffsetDateTime.parse(this).withOffsetSameInstant(OffsetDateTime.now().offset)
return context.getString(com.instructure.pandares.R.string.submissionDetailsDueAt, dueDateTime.getShortMonthAndDay(), dueDateTime.getTime())
}
+
+fun BroadcastReceiver.goAsync(
+ context: CoroutineContext = EmptyCoroutineContext,
+ block: suspend CoroutineScope.() -> Unit
+) {
+ val pendingResult = goAsync()
+ @OptIn(DelicateCoroutinesApi::class)
+ GlobalScope.launch(context) {
+ try {
+ block()
+ } finally {
+ pendingResult.finish()
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt b/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt
index 4b77b5f92d..b3fd8de5ee 100644
--- a/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt
@@ -19,7 +19,6 @@ package com.instructure.student.util
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Course
-import com.instructure.canvasapi2.models.Page
import com.instructure.canvasapi2.models.Tab
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.ContextKeeper
@@ -36,7 +35,11 @@ import com.instructure.student.features.pages.details.PageDetailsFragment
import com.instructure.student.features.pages.list.PageListFragment
import com.instructure.student.features.people.list.PeopleListFragment
import com.instructure.student.features.quiz.list.QuizListFragment
-import com.instructure.student.fragment.*
+import com.instructure.student.fragment.AnnouncementListFragment
+import com.instructure.student.fragment.CourseSettingsFragment
+import com.instructure.student.fragment.LtiLaunchFragment
+import com.instructure.student.fragment.NotificationListFragment
+import com.instructure.student.fragment.UnsupportedTabFragment
import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment
import com.instructure.student.mobius.syllabus.ui.SyllabusRepositoryFragment
import java.util.*
@@ -97,7 +100,7 @@ object TabHelper {
Tab.ASSIGNMENTS_ID -> AssignmentListFragment.makeRoute(canvasContext)
Tab.MODULES_ID -> ModuleListFragment.makeRoute(canvasContext)
Tab.PAGES_ID -> PageListFragment.makeRoute(canvasContext, false)
- Tab.FRONT_PAGE_ID -> PageDetailsFragment.makeRoute(canvasContext, Page.FRONT_PAGE_NAME)
+ Tab.FRONT_PAGE_ID -> PageDetailsFragment.makeFrontPageRoute(canvasContext)
Tab.DISCUSSIONS_ID -> DiscussionListFragment.makeRoute(canvasContext)
Tab.PEOPLE_ID -> PeopleListFragment.makeRoute(canvasContext)
Tab.FILES_ID -> FileListFragment.makeRoute(canvasContext)
diff --git a/apps/student/src/main/res/layout/dialog_custom_reminder.xml b/apps/student/src/main/res/layout/dialog_custom_reminder.xml
new file mode 100644
index 0000000000..3f5ad32e2c
--- /dev/null
+++ b/apps/student/src/main/res/layout/dialog_custom_reminder.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/student/src/main/res/layout/fragment_assignment_details.xml b/apps/student/src/main/res/layout/fragment_assignment_details.xml
index 37953ecadd..c4e135faa7 100644
--- a/apps/student/src/main/res/layout/fragment_assignment_details.xml
+++ b/apps/student/src/main/res/layout/fragment_assignment_details.xml
@@ -1,4 +1,19 @@
-
+
@@ -281,6 +296,77 @@
android:visibility="@{viewModel.data.dueDate.empty ? View.GONE : View.VISIBLE}"
app:layout_constraintTop_toBottomOf="@id/dueLabel" />
+
+
+
+
+
+
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/reminderBottomDivider" />
+
+
\ No newline at end of file
diff --git a/apps/student/src/main/res/layout/view_reminder.xml b/apps/student/src/main/res/layout/view_reminder.xml
new file mode 100644
index 0000000000..806327e22d
--- /dev/null
+++ b/apps/student/src/main/res/layout/view_reminder.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModelTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModelTest.kt
index cd3341086c..0cdb25a7cf 100644
--- a/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModelTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModelTest.kt
@@ -24,6 +24,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
+import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CanvasContext
@@ -33,11 +34,13 @@ import com.instructure.canvasapi2.models.Enrollment
import com.instructure.canvasapi2.models.LockInfo
import com.instructure.canvasapi2.models.Quiz
import com.instructure.canvasapi2.models.Submission
+import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.ContextKeeper
import com.instructure.canvasapi2.utils.toApiString
import com.instructure.pandautils.R
import com.instructure.pandautils.mvvm.ViewState
+import com.instructure.pandautils.room.appdatabase.entities.ReminderEntity
import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.Const
import com.instructure.pandautils.utils.HtmlContentFormatter
@@ -45,8 +48,12 @@ import com.instructure.student.db.StudentDb
import com.instructure.student.features.assignments.details.AssignmentDetailAction
import com.instructure.student.features.assignments.details.AssignmentDetailsRepository
import com.instructure.student.features.assignments.details.AssignmentDetailsViewModel
+import com.instructure.student.features.assignments.details.ReminderChoice
+import com.instructure.student.features.assignments.details.ReminderViewData
import com.instructure.student.features.assignments.details.gradecellview.GradeCellViewData
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
@@ -81,6 +88,7 @@ class AssignmentDetailsViewModelTest {
private val application: Application = mockk(relaxed = true)
private val apiPrefs: ApiPrefs = mockk(relaxed = true)
private val database: StudentDb = mockk(relaxed = true)
+ private val alarmScheduler: AlarmScheduler = mockk(relaxed = true)
@Before
fun setUp() {
@@ -97,6 +105,9 @@ class AssignmentDetailsViewModelTest {
every { savedStateHandle.get(Const.CANVAS_CONTEXT) } returns Course()
every { savedStateHandle.get(Const.ASSIGNMENT_ID) } returns 0L
+
+ every { assignmentDetailsRepository.getRemindersByAssignmentIdLiveData(any(), any()) } returns MutableLiveData()
+ every { apiPrefs.user } returns User(id = 1)
}
fun tearDown() {
@@ -111,6 +122,7 @@ class AssignmentDetailsViewModelTest {
colorKeeper,
application,
apiPrefs,
+ alarmScheduler,
database
)
@@ -775,4 +787,196 @@ class AssignmentDetailsViewModelTest {
Assert.assertFalse(viewModel.data.value?.submitVisible!!)
}
+
+ @Test
+ fun `Reminder section is not visible if there's no future deadline`() {
+ val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))
+ coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course
+
+ val assignment = Assignment(name = "Test", submissionTypesRaw = listOf("online_text_entry"))
+ coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment
+
+ val viewModel = getViewModel()
+
+ Assert.assertFalse(viewModel.data.value?.showReminders!!)
+ }
+
+ @Test
+ fun `Reminder section visible if there's a future deadline`() {
+ val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))
+ coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course
+
+ val assignment = Assignment(
+ name = "Test",
+ submissionTypesRaw = listOf("online_text_entry"),
+ dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString()
+ )
+ coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment
+
+ val viewModel = getViewModel()
+
+ Assert.assertTrue(viewModel.data.value?.showReminders!!)
+ }
+
+ @Test
+ fun `Reminders map correctly`() {
+ val reminderEntities = listOf(
+ ReminderEntity(1, 1, 1, "htmlUrl1", "Assignment 1", "1 day", 1000),
+ ReminderEntity(2, 1, 1, "htmlUrl2", "Assignment 2", "2 days", 2000),
+ ReminderEntity(3, 1, 1, "htmlUrl3", "Assignment 3", "3 days", 3000)
+ )
+ val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))
+ coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course
+ every { assignmentDetailsRepository.getRemindersByAssignmentIdLiveData(any(), any()) } returns MutableLiveData(reminderEntities)
+ every { resources.getString(eq(R.string.reminderBefore), any()) } answers { call -> "${(call.invocation.args[1] as Array<*>)[0]} Before" }
+
+ val assignment = Assignment(
+ name = "Test",
+ submissionTypesRaw = listOf("online_text_entry"),
+ dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString()
+ )
+ coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment
+
+ val viewModel = getViewModel()
+
+ Assert.assertEquals(
+ reminderEntities.map { ReminderViewData(it.id, "${it.text} Before") },
+ viewModel.data.value?.reminders?.map { it.data }
+ )
+ }
+
+ @Test
+ fun `Reminders update correctly`() {
+ val remindersLiveData = MutableLiveData>()
+ val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))
+ coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course
+ every { assignmentDetailsRepository.getRemindersByAssignmentIdLiveData(any(), any()) } returns remindersLiveData
+ every { resources.getString(eq(R.string.reminderBefore), any()) } answers { call -> "${(call.invocation.args[1] as Array<*>)[0]} Before" }
+
+ val assignment = Assignment(
+ name = "Test",
+ submissionTypesRaw = listOf("online_text_entry"),
+ dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString()
+ )
+ coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment
+
+ val viewModel = getViewModel()
+
+ Assert.assertEquals(0, viewModel.data.value?.reminders?.size)
+
+ remindersLiveData.value = listOf(ReminderEntity(1, 1, 1, "htmlUrl1", "Assignment 1", "1 day", 1000))
+
+ Assert.assertEquals(ReminderViewData(1, "1 day Before"), viewModel.data.value?.reminders?.first()?.data)
+ }
+
+ @Test
+ fun `Add reminder posts action`() {
+ val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))
+ coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course
+
+ val assignment = Assignment(
+ name = "Test",
+ submissionTypesRaw = listOf("online_text_entry"),
+ dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString()
+ )
+ coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment
+
+ val viewModel = getViewModel()
+
+ viewModel.onAddReminderClicked()
+
+ Assert.assertEquals(AssignmentDetailAction.ShowReminderDialog, viewModel.events.value?.peekContent())
+ }
+
+ @Test
+ fun `Selected reminder choice`() {
+ val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))
+ coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course
+ every { savedStateHandle.get(Const.ASSIGNMENT_ID) } returns 1
+ every { resources.getQuantityString(R.plurals.reminderDay, 3, 3) } returns "3 days"
+
+ val assignment = Assignment(
+ name = "Test",
+ submissionTypesRaw = listOf("online_text_entry"),
+ dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 4) }.time.toApiString()
+ )
+ coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment
+
+ val viewModel = getViewModel()
+
+ viewModel.onReminderSelected(ReminderChoice.Day(3))
+
+ val time = assignment.dueDate?.time?.minus(3 * 24 * 60 * 60 * 1000L)
+
+ coVerify(exactly = 1) {
+ assignmentDetailsRepository.addReminder(1, assignment, "3 days", time!!)
+ }
+ }
+
+ @Test
+ fun `Selected reminder choice custom`() {
+ val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))
+ coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course
+
+ val assignment = Assignment(
+ name = "Test",
+ submissionTypesRaw = listOf("online_text_entry"),
+ dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString()
+ )
+ coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment
+
+ val viewModel = getViewModel()
+
+ viewModel.onReminderSelected(ReminderChoice.Custom)
+
+ Assert.assertEquals(AssignmentDetailAction.ShowCustomReminderDialog, viewModel.events.value?.peekContent())
+ }
+
+ @Test
+ fun `Selected past reminder choice`() {
+ val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))
+ coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course
+ every { savedStateHandle.get(Const.ASSIGNMENT_ID) } returns 1
+ every { resources.getQuantityString(R.plurals.reminderDay, 3, 3) } returns "3 days"
+ every { resources.getString(R.string.reminderInPast) } returns "Reminder in past"
+
+ val assignment = Assignment(
+ name = "Test",
+ submissionTypesRaw = listOf("online_text_entry"),
+ dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 2) }.time.toApiString()
+ )
+ coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment
+
+ val viewModel = getViewModel()
+
+ viewModel.onReminderSelected(ReminderChoice.Day(3))
+
+ Assert.assertEquals(AssignmentDetailAction.ShowToast("Reminder in past"), viewModel.events.value?.peekContent())
+ }
+
+ @Test
+ fun `Selected reminder already set up`() {
+ val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))
+ coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course
+ every { savedStateHandle.get(Const.ASSIGNMENT_ID) } returns 1
+ every { resources.getQuantityString(R.plurals.reminderDay, 3, 3) } returns "3 days"
+ every { resources.getString(R.string.reminderAlreadySet) } returns "Reminder in past"
+ val assignment = Assignment(
+ name = "Test",
+ submissionTypesRaw = listOf("online_text_entry"),
+ dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 4) }.time.toApiString()
+ )
+ coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment
+ val time = assignment.dueDate?.time?.minus(3 * 24 * 60 * 60 * 1000L)
+ val reminderEntities = listOf(
+ ReminderEntity(1, 1, 1, "htmlUrl1", "Assignment 1", "1 day", time!!)
+ )
+ every { assignmentDetailsRepository.getRemindersByAssignmentIdLiveData(any(), any()) } returns MutableLiveData(reminderEntities)
+
+ val viewModel = getViewModel()
+
+ viewModel.onReminderSelected(ReminderChoice.Day(3))
+
+ Assert.assertEquals(AssignmentDetailAction.ShowToast("Reminder in past"), viewModel.events.value?.peekContent())
+ }
}
diff --git a/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/reminder/AlarmSchedulerTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/reminder/AlarmSchedulerTest.kt
new file mode 100644
index 0000000000..c19a555fb9
--- /dev/null
+++ b/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/reminder/AlarmSchedulerTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 - 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.student.features.assignmentdetails.reminder
+
+import android.app.AlarmManager
+import android.content.Context
+import com.instructure.canvasapi2.models.User
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
+import com.instructure.pandautils.room.appdatabase.entities.ReminderEntity
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.spyk
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+
+class AlarmSchedulerTest {
+
+ private val context: Context = mockk(relaxed = true)
+ private val reminderDao: ReminderDao = mockk(relaxed = true)
+ private val apiPrefs: ApiPrefs = mockk(relaxed = true)
+ private val alarmManager: AlarmManager = mockk(relaxed = true)
+
+ @Before
+ fun setup() {
+ every { context.getSystemService(Context.ALARM_SERVICE) } returns alarmManager
+ }
+
+ @Test
+ fun `Test schedule all alarms for the current user`() = runTest {
+ val alarmScheduler = spyk(AlarmScheduler(context, reminderDao, apiPrefs))
+
+ val reminder1 = ReminderEntity(1, 1, 1, "path1", "Assignment 1", "1 day", 12345678)
+ val reminder2 = ReminderEntity(2, 1, 2, "path2", "Assignment 2", "2 hours", 12345678)
+
+ every { apiPrefs.user } returns User(id = 1)
+ coEvery { reminderDao.findByUserId(1) } returns listOf(reminder1, reminder2)
+
+ coEvery { alarmScheduler.scheduleAlarm(any(), any(), any(), any(), any(), any()) } just Runs
+ coEvery { alarmScheduler.scheduleAllAlarmsForCurrentUser() } answers { callOriginal() }
+
+ alarmScheduler.scheduleAllAlarmsForCurrentUser()
+
+ coVerify {
+ alarmScheduler.scheduleAlarm(reminder1.assignmentId, reminder1.htmlUrl, reminder1.name, reminder1.text, reminder1.time, reminder1.id)
+ alarmScheduler.scheduleAlarm(reminder2.assignmentId, reminder2.htmlUrl, reminder2.name, reminder2.text, reminder2.time, reminder2.id)
+ }
+ }
+
+ @Test
+ fun `Test cancel all alarms for the current user`() = runTest {
+ val alarmScheduler = spyk(AlarmScheduler(context, reminderDao, apiPrefs))
+
+ val reminder1 = ReminderEntity(1, 1, 1, "path1", "Assignment 1", "1 day", 12345678)
+ val reminder2 = ReminderEntity(2, 1, 2, "path2", "Assignment 2", "2 hours", 12345678)
+
+ every { apiPrefs.user } returns User(id = 1)
+ coEvery { reminderDao.findByUserId(1) } returns listOf(reminder1, reminder2)
+
+ coEvery { alarmScheduler.cancelAlarm(any()) } just Runs
+ coEvery { alarmScheduler.cancelAllAlarmsForCurrentUser() } answers { callOriginal() }
+
+ alarmScheduler.cancelAllAlarmsForCurrentUser()
+
+ coVerify {
+ alarmScheduler.cancelAlarm(reminder1.id)
+ alarmScheduler.cancelAlarm(reminder2.id)
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsRepositoryTest.kt
index f569008123..ef8d127a38 100644
--- a/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsRepositoryTest.kt
@@ -17,11 +17,13 @@
package com.instructure.student.features.offline.assignmentdetails
+import androidx.lifecycle.MutableLiveData
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.LTITool
import com.instructure.canvasapi2.models.Quiz
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
+import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
+import com.instructure.pandautils.room.appdatabase.entities.ReminderEntity
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.assignments.details.AssignmentDetailsRepository
@@ -44,8 +46,9 @@ class AssignmentDetailsRepositoryTest {
private val localDataSource: AssignmentDetailsLocalDataSource = mockk(relaxed = true)
private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true)
private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true)
+ private val reminderDao: ReminderDao = mockk(relaxed = true)
- private val repository = AssignmentDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider)
+ private val repository = AssignmentDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider, reminderDao)
@Before
fun setup() = runTest {
@@ -179,4 +182,41 @@ class AssignmentDetailsRepositoryTest {
Assert.assertEquals(null, ltiTool)
}
+
+ @Test
+ fun `Get reminders liveData`() = runTest {
+ val expected = MutableLiveData>()
+ every { reminderDao.findByAssignmentIdLiveData(any(), any()) } returns expected
+
+ val reminderLiveData = repository.getRemindersByAssignmentIdLiveData(1, 1)
+
+ Assert.assertEquals(expected, reminderLiveData)
+ }
+
+ @Test
+ fun `Delete reminder`() = runTest {
+ repository.deleteReminderById(1)
+
+ coVerify(exactly = 1) {
+ reminderDao.deleteById(1)
+ }
+ }
+
+ @Test
+ fun `Add reminder`() = runTest {
+ repository.addReminder(1, Assignment(1, name = "Assignment 1", htmlUrl = "htmlUrl"), "Test Reminder", 1000)
+
+ coVerify(exactly = 1) {
+ reminderDao.insert(
+ ReminderEntity(
+ userId = 1,
+ assignmentId = 1,
+ htmlUrl = "htmlUrl",
+ name = "Assignment 1",
+ text = "Test Reminder",
+ time = 1000
+ )
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle
index 734a9e30ab..d58c3fc40c 100644
--- a/apps/teacher/build.gradle
+++ b/apps/teacher/build.gradle
@@ -39,8 +39,8 @@ android {
defaultConfig {
minSdkVersion Versions.MIN_SDK
targetSdkVersion Versions.TARGET_SDK
- versionCode = 63
- versionName = '1.28.1'
+ versionCode = 64
+ versionName = '1.29.0'
vectorDrawables.useSupportLibrary = true
multiDexEnabled true
testInstrumentationRunner 'com.instructure.teacher.ui.espresso.TeacherHiltTestRunner'
@@ -111,6 +111,15 @@ android {
heapEnabled = true
}
}
+
+ debugMinify {
+ initWith debug
+ debuggable false
+ minifyEnabled true
+ shrinkResources true
+ matchingFallbacks = ['debug']
+ }
+
release {
minifyEnabled true
shrinkResources true
@@ -311,6 +320,8 @@ dependencies {
implementation Libs.ROOM_COROUTINES
testImplementation Libs.HAMCREST
+
+ androidTestImplementation Libs.COMPOSE_UI_TEST
}
apply plugin: 'com.google.gms.google-services'
diff --git a/apps/teacher/flank.yml b/apps/teacher/flank.yml
index 97639ae19b..eafa4e03e4 100644
--- a/apps/teacher/flank.yml
+++ b/apps/teacher/flank.yml
@@ -1,5 +1,8 @@
gcloud:
project: delta-essence-114723
+# Use the next two lines to run locally
+# app: ./build/intermediates/apk/qa/debug/teacher-qa-debug.apk
+# test: ./build/intermediates/apk/androidTest/qa/debug/teacher-qa-debug-androidTest.apk
app: ./apps/teacher/build/outputs/apk/qa/debug/teacher-qa-debug.apk
test: ./apps/teacher/build/outputs/apk/androidTest/qa/debug/teacher-qa-debug-androidTest.apk
results-bucket: android-teacher
diff --git a/apps/teacher/flank_coverage.yml b/apps/teacher/flank_coverage.yml
index c23ecac71e..3d23a78f48 100644
--- a/apps/teacher/flank_coverage.yml
+++ b/apps/teacher/flank_coverage.yml
@@ -19,15 +19,15 @@ gcloud:
directories-to-pull:
- /sdcard/
test-targets:
- - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub
+ - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage
device:
- - model: NexusLowRes
- version: 26
+ - model: Pixel2.arm
+ version: 29
locale: en_US
orientation: portrait
flank:
- testShards: 16
+ testShards: 10
testRuns: 1
files-to-download:
- .*\.ec$
diff --git a/apps/teacher/flank_e2e_coverage.yml b/apps/teacher/flank_e2e_coverage.yml
index eea5ec934e..af3fe15179 100644
--- a/apps/teacher/flank_e2e_coverage.yml
+++ b/apps/teacher/flank_e2e_coverage.yml
@@ -20,10 +20,10 @@ gcloud:
- /sdcard/
test-targets:
- annotation com.instructure.canvas.espresso.E2E
- - notAnnotation com.instructure.canvas.espresso.Stub
+ - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage
device:
- - model: Nexus6P
- version: 26
+ - model: Pixel2.arm
+ version: 29
locale: en_US
orientation: portrait
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDetailsPageTest.kt
index 935ec1fd2b..33fb1837ee 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDetailsPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDetailsPageTest.kt
@@ -32,7 +32,6 @@ import com.instructure.dataseeding.util.ago
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.espresso.TestRail
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -42,7 +41,6 @@ import org.junit.Test
class AssignmentDetailsPageTest : TeacherTest() {
@Test
- @TestRail(ID = "C3109579")
override fun displaysPageObjects() {
getToAssignmentDetailsPage(
submissionTypes = listOf(ONLINE_TEXT_ENTRY),
@@ -52,35 +50,30 @@ class AssignmentDetailsPageTest : TeacherTest() {
}
@Test
- @TestRail(ID = "C3109579")
fun displaysCorrectDetails() {
val assignment = getToAssignmentDetailsPage()
assignmentDetailsPage.assertAssignmentDetails(assignment)
}
@Test
- @TestRail(ID = "C3109579")
fun displaysInstructions() {
getToAssignmentDetailsPage(withDescription = true)
assignmentDetailsPage.assertDisplaysInstructions()
}
@Test
- @TestRail(ID = "C3134480")
fun displaysNoInstructionsMessage() {
getToAssignmentDetailsPage()
assignmentDetailsPage.assertDisplaysNoInstructionsView()
}
@Test
- @TestRail(ID = "C3134481")
fun displaysClosedAvailability() {
getToAssignmentDetailsPage(lockAt = 7.days.ago.iso8601)
assignmentDetailsPage.assertAssignmentClosed()
}
@Test
- @TestRail(ID = "C313448 2")
fun displaysNoFromDate() {
val lockAt = 7.days.fromNow.iso8601
getToAssignmentDetailsPage(lockAt = lockAt)
@@ -88,7 +81,6 @@ class AssignmentDetailsPageTest : TeacherTest() {
}
@Test
- @TestRail(ID = "C3134483")
fun displaysNoToDate() {
getToAssignmentDetailsPage(unlockAt = 7.days.ago.iso8601)
assignmentDetailsPage.assertFromFilledAndToEmpty()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDueDatesPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDueDatesPageTest.kt
index ca15ef0125..832ffce0e6 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDueDatesPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDueDatesPageTest.kt
@@ -25,7 +25,6 @@ import com.instructure.dataseeding.util.ago
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.espresso.TestRail
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -35,28 +34,24 @@ import org.junit.Test
class AssignmentDueDatesPageTest : TeacherTest() {
@Test
- @TestRail(ID = "C3134131")
override fun displaysPageObjects() {
getToDueDatesPage()
assignmentDueDatesPage.assertPageObjects()
}
@Test
- @TestRail(ID = "C3134484")
fun displaysNoDueDate() {
getToDueDatesPage()
assignmentDueDatesPage.assertDisplaysNoDueDate()
}
@Test
- @TestRail(ID = "C3134485")
fun displaysSingleDueDate() {
getToDueDatesPage(dueAt = 7.days.fromNow.iso8601)
assignmentDueDatesPage.assertDisplaysSingleDueDate()
}
@Test
- @TestRail(ID = "C3134486")
fun displaysAvailabilityDates() {
getToDueDatesPage(lockAt = 7.days.fromNow.iso8601, unlockAt = 7.days.ago.iso8601)
assignmentDueDatesPage.assertDisplaysAvailabilityDates()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentListPageTest.kt
index 545c030706..aed434808d 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentListPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentListPageTest.kt
@@ -22,7 +22,6 @@ import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.AssignmentGroup
import com.instructure.canvasapi2.models.CanvasContextPermission
-import com.instructure.espresso.TestRail
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -32,28 +31,24 @@ import org.junit.Test
class AssignmentListPageTest : TeacherTest() {
@Test
- @TestRail(ID = "C3109578")
override fun displaysPageObjects() {
getToAssignmentsPage()
assignmentListPage.assertPageObjects()
}
@Test
- @TestRail(ID = "C3134487")
fun displaysNoAssignmentsView() {
getToAssignmentsPage(0)
assignmentListPage.assertDisplaysNoAssignmentsView()
}
@Test
- @TestRail(ID = "C3109578")
fun displaysAssignment() {
val assignment = getToAssignmentsPage().assignments.values.first()
assignmentListPage.assertHasAssignment(assignment)
}
@Test
- @TestRail(ID = "C3134488")
fun displaysGradingPeriods() {
getToAssignmentsPage(gradingPeriods = true)
assignmentListPage.assertHasGradingPeriods()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseBrowserPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseBrowserPageTest.kt
index d79890cb61..23fab0637c 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseBrowserPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseBrowserPageTest.kt
@@ -17,7 +17,6 @@ package com.instructure.teacher.ui
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.init
-import com.instructure.espresso.TestRail
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -27,7 +26,6 @@ import org.junit.Test
class CourseBrowserPageTest : TeacherTest() {
@Test
- @TestRail(ID = "C3108909")
override fun displaysPageObjects() {
val data = MockCanvas.init(teacherCount = 1, courseCount = 3, favoriteCourseCount = 3)
val teacher = data.teachers[0]
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseSettingsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseSettingsPageTest.kt
index 03a0ce089a..293c165756 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseSettingsPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseSettingsPageTest.kt
@@ -17,7 +17,6 @@ package com.instructure.teacher.ui
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.init
-import com.instructure.espresso.TestRail
import com.instructure.espresso.randomString
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.tokenLogin
@@ -28,14 +27,12 @@ import org.junit.Test
class CourseSettingsPageTest : TeacherTest() {
@Test
- @TestRail(ID = "C3108914")
override fun displaysPageObjects() {
navigateToCourseSettings()
courseSettingsPage.assertPageObjects()
}
@Test
- @TestRail(ID = "C3108915")
fun editCourseName() {
navigateToCourseSettings()
courseSettingsPage.clickCourseName()
@@ -45,7 +42,6 @@ class CourseSettingsPageTest : TeacherTest() {
}
@Test
- @TestRail(ID = "C3108916")
fun editCourseHomePage() {
navigateToCourseSettings()
courseSettingsPage.clickSetHomePage()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DashboardPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DashboardPageTest.kt
index e7c8834be9..ed8c118f6c 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DashboardPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DashboardPageTest.kt
@@ -19,7 +19,6 @@ package com.instructure.teacher.ui
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.init
-import com.instructure.espresso.TestRail
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -29,7 +28,6 @@ import org.junit.Test
class DashboardPageTest : TeacherTest() {
@Test
- @TestRail(ID = "C3108898")
override fun displaysPageObjects() {
val data = MockCanvas.init(teacherCount = 1, courseCount = 1, favoriteCourseCount = 1)
val teacher = data.teachers[0]
@@ -39,7 +37,6 @@ class DashboardPageTest : TeacherTest() {
}
@Test
- @TestRail(ID = "C3109494")
fun displaysNoCoursesView() {
val data = MockCanvas.init(teacherCount = 1, pastCourseCount = 1)
val teacher = data.teachers[0]
@@ -49,7 +46,6 @@ class DashboardPageTest : TeacherTest() {
}
@Test
- @TestRail(ID = "C3108898")
fun displaysCourseList() {
val data = MockCanvas.init(teacherCount = 1, favoriteCourseCount = 3, courseCount = 3)
val teacher = data.teachers[0]
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditAssignmentDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditAssignmentDetailsPageTest.kt
index 4713c578b8..8b529305da 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditAssignmentDetailsPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditAssignmentDetailsPageTest.kt
@@ -25,7 +25,6 @@ import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CanvasContextPermission
import com.instructure.canvasapi2.utils.NumberHelper
-import com.instructure.espresso.TestRail
import com.instructure.espresso.randomDouble
import com.instructure.espresso.randomString
import com.instructure.teacher.R
@@ -39,7 +38,6 @@ import org.junit.Test
class EditAssignmentDetailsPageTest : TeacherTest() {
@Test
- @TestRail(ID = "C3109580")
override fun displaysPageObjects() {
getToEditAssignmentDetailsPage()
editAssignmentDetailsPage.assertPageObjects()
@@ -55,7 +53,6 @@ class EditAssignmentDetailsPageTest : TeacherTest() {
}
@Test
- @TestRail(ID = "C3134126")
fun editAssignmentName() {
getToEditAssignmentDetailsPage()
editAssignmentDetailsPage.clickAssignmentNameEditText()
@@ -66,7 +63,6 @@ class EditAssignmentDetailsPageTest : TeacherTest() {
}
@Test
- @TestRail(ID = "C3134126")
fun editAssignmentPoints() {
getToEditAssignmentDetailsPage()
editAssignmentDetailsPage.clickPointsPossibleEditText()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditDashboardPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditDashboardPageTest.kt
index 6ac25bf8cc..f39d3e15aa 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditDashboardPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditDashboardPageTest.kt
@@ -19,7 +19,6 @@ package com.instructure.teacher.ui
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.init
-import com.instructure.espresso.TestRail
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -29,7 +28,6 @@ import org.junit.Test
class EditDashboardPageTest : TeacherTest() {
@Test
- @TestRail(ID = "C3109572")
override fun displaysPageObjects() {
setUpAndSignIn(numCourses = 1, numFavoriteCourses = 0)
dashboardPage.clickEditDashboard()
@@ -37,7 +35,6 @@ class EditDashboardPageTest : TeacherTest() {
}
@Test
- @TestRail(ID = "C3109572")
fun displaysCourseList() {
val data = setUpAndSignIn(numCourses = 1, numFavoriteCourses = 0)
val course = data.courses.values.toList()[0]
@@ -46,7 +43,6 @@ class EditDashboardPageTest : TeacherTest() {
}
@Test
- @TestRail(ID = "C3109574")
fun addCourseToFavourites() {
val data = setUpAndSignIn(numCourses = 1, numFavoriteCourses = 0)
val courses = data.courses.values.toList()
@@ -57,7 +53,6 @@ class EditDashboardPageTest : TeacherTest() {
}
@Test
- @TestRail(ID = "C3109575")
fun removeCourseFromFavourites() {
val data = setUpAndSignIn(numCourses = 1, numFavoriteCourses = 1)
val courses = data.courses.values.toList()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InAppUpdatePageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InAppUpdatePageTest.kt
index 781de656c7..dcf6a78b7e 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InAppUpdatePageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InAppUpdatePageTest.kt
@@ -238,7 +238,6 @@ class InAppUpdatePageTest : TeacherTest() {
}
@Test
- @Stub("Stubbed because on API lvl 29 device the notification will remain opened even though we push the back button at the end. Should be investigated and make some workaround once.")
fun showNotificationOnFlexibleDownloadFinish() {
updatePrefs.clearPrefs()
val expectedTitle = context.getString(R.string.appUpdateReadyTitle)
@@ -270,8 +269,8 @@ class InAppUpdatePageTest : TeacherTest() {
}
@Test
- @Stub(description = "https://instructure.atlassian.net/browse/MBL-16824")
fun flexibleUpdateCompletesIfAppRestarts() {
+ updatePrefs.clearPrefs()
with(appUpdateManager) {
setUpdateAvailable(400)
setUpdatePriority(2)
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginFindSchoolPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginFindSchoolPageTest.kt
index 0b580f2304..48bdaf59c6 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginFindSchoolPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginFindSchoolPageTest.kt
@@ -1,6 +1,5 @@
package com.instructure.teacher.ui
-import com.instructure.espresso.TestRail
import com.instructure.teacher.ui.utils.TeacherTest
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
@@ -9,7 +8,6 @@ import org.junit.Test
class LoginFindSchoolPageTest: TeacherTest() {
@Test
- @TestRail(ID = "C3108892")
override fun displaysPageObjects() {
loginLandingPage.clickFindMySchoolButton()
loginFindSchoolPage.assertPageObjects()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginLandingPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginLandingPageTest.kt
index 2b2e096153..ec1382b24c 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginLandingPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginLandingPageTest.kt
@@ -1,6 +1,5 @@
package com.instructure.teacher.ui
-import com.instructure.espresso.TestRail
import com.instructure.espresso.filters.P1
import com.instructure.teacher.ui.utils.TeacherTest
import dagger.hilt.android.testing.HiltAndroidTest
@@ -11,7 +10,6 @@ class LoginLandingPageTest: TeacherTest() {
// Runs live; no MockCanvas
@Test
- @TestRail(ID = "C3108891")
@P1
override fun displaysPageObjects() {
loginLandingPage.assertPageObjects()
@@ -19,7 +17,6 @@ class LoginLandingPageTest: TeacherTest() {
// Runs live; no MockCanvas
@Test
- @TestRail(ID = "C3108893")
fun opensCanvasNetworksSignInPage() {
loginLandingPage.clickCanvasNetworkButton()
loginSignInPage.assertPageObjects()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginSignInPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginSignInPageTest.kt
index 898252fea7..2bdbcdd5d0 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginSignInPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginSignInPageTest.kt
@@ -17,7 +17,6 @@
package com.instructure.teacher.ui
-import com.instructure.espresso.TestRail
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.enterDomain
import dagger.hilt.android.testing.HiltAndroidTest
@@ -28,7 +27,6 @@ class LoginSignInPageTest: TeacherTest() {
// Runs live; no MockCanvas
@Test
- @TestRail(ID = "C3108896")
override fun displaysPageObjects() {
loginLandingPage.clickFindMySchoolButton()
enterDomain()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ModuleListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ModuleListPageTest.kt
new file mode 100644
index 0000000000..6dea81f0a7
--- /dev/null
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ModuleListPageTest.kt
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.instructure.teacher.ui
+
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addAssignment
+import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
+import com.instructure.canvas.espresso.mockCanvas.addFileToCourse
+import com.instructure.canvas.espresso.mockCanvas.addItemToModule
+import com.instructure.canvas.espresso.mockCanvas.addModuleToCourse
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CanvasContextPermission
+import com.instructure.canvasapi2.models.ModuleContentDetails
+import com.instructure.canvasapi2.models.Tab
+import com.instructure.dataseeding.util.Randomizer
+import com.instructure.teacher.R
+import com.instructure.teacher.ui.utils.TeacherComposeTest
+import com.instructure.teacher.ui.utils.openOverflowMenu
+import com.instructure.teacher.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Test
+
+@HiltAndroidTest
+class ModuleListPageTest : TeacherComposeTest() {
+
+ @Test
+ override fun displaysPageObjects() {
+ goToModulesPage()
+ moduleListPage.assertPageObjects()
+ }
+
+ @Test
+ fun assertDisplaysMenuItems() {
+ goToModulesPage()
+ openOverflowMenu()
+ moduleListPage.assertToolbarMenuItems()
+ }
+
+ @Test
+ fun assertDisplaysModuleMenuItems() {
+ val data = goToModulesPage()
+ val module = data.courseModules.values.first().first()
+
+ moduleListPage.clickItemOverflow(module.name.orEmpty())
+ moduleListPage.assertModuleMenuItems()
+ }
+
+ @Test
+ fun assertPublishedItemActions() {
+ val data = goToModulesPage()
+ val module = data.courseModules.values.first().first()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(
+ courseId = course.id,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ )
+
+ data.addItemToModule(data.courses.values.first(), module.id, assignment, published = true)
+
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(assignment.name.orEmpty())
+ moduleListPage.assertOverflowItem(R.string.unpublish)
+ }
+
+ @Test
+ fun assertUnpublishedItemActions() {
+ val data = goToModulesPage()
+ val module = data.courseModules.values.first().first()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(
+ courseId = course.id,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ )
+
+ data.addItemToModule(data.courses.values.first(), module.id, assignment, published = false)
+
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(assignment.name.orEmpty())
+ moduleListPage.assertOverflowItem(R.string.publish)
+ }
+
+ @Test
+ fun assertFileEditOpens() {
+ val data = goToModulesPage()
+ val module = data.courseModules.values.first().first()
+ val course = data.courses.values.first()
+ val fileId = data.addFileToCourse(course.id)
+ val rootFolderId = data.courseRootFolders[course.id]!!.id
+ val fileFolder = data.folderFiles[rootFolderId]?.find { it.id == fileId }
+ data.addItemToModule(
+ course = course,
+ moduleId = module.id,
+ item = fileFolder!!
+ )
+
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(fileFolder.displayName.orEmpty())
+
+ moduleListPage.assertFileEditDialogVisible()
+ }
+
+ @Test
+ fun publishModuleItem() {
+ val data = goToModulesPage()
+ val module = data.courseModules.values.first().first()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(
+ courseId = course.id,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ )
+
+ data.addItemToModule(data.courses.values.first(), module.id, assignment, published = false)
+
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(assignment.name.orEmpty())
+ moduleListPage.clickOnText(R.string.publishModuleItemAction)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ moduleListPage.assertSnackbarText(R.string.moduleItemPublished)
+ moduleListPage.assertModuleItemIsPublished(assignment.name.orEmpty())
+ }
+
+ @Test
+ fun unpublishModuleItem() {
+ val data = goToModulesPage()
+ val module = data.courseModules.values.first().first()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(
+ courseId = course.id,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ )
+
+ data.addItemToModule(data.courses.values.first(), module.id, assignment, published = true)
+
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(assignment.name.orEmpty())
+ moduleListPage.clickOnText(R.string.unpublishModuleItemAction)
+ moduleListPage.clickOnText(R.string.unpublishDialogPositiveButton)
+
+ moduleListPage.assertSnackbarText(R.string.moduleItemUnpublished)
+ moduleListPage.assertModuleItemNotPublished(assignment.name.orEmpty())
+ }
+
+ @Test
+ fun publishModuleOnly() {
+ val data = goToModulesPage(publishedModuleCount = 0, unpublishedModuleCount = 1)
+ val unpublishedModule = data.courseModules.values.first().first { it.published == false }
+ val assignment = data.addAssignment(courseId = data.courses.values.first().id)
+
+ data.addItemToModule(data.courses.values.first(), unpublishedModule.id, assignment, published = false)
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(unpublishedModule.name.orEmpty())
+ moduleListPage.clickOnText(R.string.publishModuleOnly)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ moduleListPage.assertSnackbarText(R.string.onlyModulePublished)
+ moduleListPage.assertModuleIsPublished(unpublishedModule.name.orEmpty())
+ moduleListPage.assertModuleItemNotPublished(assignment.name.orEmpty())
+ }
+
+ @Test
+ fun publishModuleAndItems() {
+ val data = goToModulesPage(publishedModuleCount = 0, unpublishedModuleCount = 1)
+ val unpublishedModule = data.courseModules.values.first().first { it.published == false }
+ val assignment = data.addAssignment(courseId = data.courses.values.first().id)
+
+ data.addItemToModule(data.courses.values.first(), unpublishedModule.id, assignment, published = false)
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(unpublishedModule.name.orEmpty())
+ moduleListPage.clickOnText(R.string.publishModuleAndItems)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ progressPage.clickDone()
+
+ moduleListPage.assertSnackbarText(R.string.moduleAndAllItemsPublished)
+ moduleListPage.assertModuleIsPublished(unpublishedModule.name.orEmpty())
+ moduleListPage.assertModuleItemIsPublished(assignment.name.orEmpty())
+ }
+
+ @Test
+ fun unpublishModuleAndItems() {
+ val data = goToModulesPage(publishedModuleCount = 1, unpublishedModuleCount = 0)
+ val publishedModule = data.courseModules.values.first().first { it.published == true }
+ val assignment = data.addAssignment(courseId = data.courses.values.first().id)
+
+ data.addItemToModule(data.courses.values.first(), publishedModule.id, assignment, published = true)
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(publishedModule.name.orEmpty())
+ moduleListPage.clickOnText(R.string.unpublishModuleAndItems)
+ moduleListPage.clickOnText(R.string.unpublishDialogPositiveButton)
+
+ progressPage.clickDone()
+
+ moduleListPage.assertSnackbarText(R.string.moduleAndAllItemsUnpublished)
+ moduleListPage.assertModuleNotPublished(publishedModule.name.orEmpty())
+ moduleListPage.assertModuleItemNotPublished(assignment.name.orEmpty())
+ }
+
+ @Test
+ fun publishModulesOnly() {
+ val data = goToModulesPage(publishedModuleCount = 0, unpublishedModuleCount = 2)
+ val unpublishedModules = data.courseModules.values.first().filter { it.published == false }
+ val assignment1 = data.addAssignment(courseId = data.courses.values.first().id)
+ val assignment2 = data.addAssignment(courseId = data.courses.values.first().id)
+
+ data.addItemToModule(data.courses.values.first(), unpublishedModules[0].id, assignment1, published = false)
+ data.addItemToModule(data.courses.values.first(), unpublishedModules[1].id, assignment2, published = false)
+
+ moduleListPage.refresh()
+
+ openOverflowMenu()
+ moduleListPage.clickOnText(R.string.publishModulesOnly)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ progressPage.clickDone()
+
+ moduleListPage.assertSnackbarText(R.string.onlyModulesPublished)
+ moduleListPage.assertModuleIsPublished(unpublishedModules[0].name.orEmpty())
+ moduleListPage.assertModuleIsPublished(unpublishedModules[1].name.orEmpty())
+ moduleListPage.assertModuleItemNotPublished(assignment1.name.orEmpty())
+ moduleListPage.assertModuleItemNotPublished(assignment2.name.orEmpty())
+ }
+
+ @Test
+ fun publishModulesAndItems() {
+ val data = goToModulesPage(publishedModuleCount = 0, unpublishedModuleCount = 2)
+ val unpublishedModules = data.courseModules.values.first().filter { it.published == false }
+ val assignment1 = data.addAssignment(courseId = data.courses.values.first().id)
+ val assignment2 = data.addAssignment(courseId = data.courses.values.first().id)
+
+ data.addItemToModule(data.courses.values.first(), unpublishedModules[0].id, assignment1, published = false)
+ data.addItemToModule(data.courses.values.first(), unpublishedModules[1].id, assignment2, published = false)
+
+ moduleListPage.refresh()
+
+ openOverflowMenu()
+ moduleListPage.clickOnText(R.string.publishAllModulesAndItems)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ progressPage.clickDone()
+
+ moduleListPage.assertSnackbarText(R.string.allModulesAndAllItemsPublished)
+ moduleListPage.assertModuleIsPublished(unpublishedModules[0].name.orEmpty())
+ moduleListPage.assertModuleIsPublished(unpublishedModules[1].name.orEmpty())
+ moduleListPage.assertModuleItemIsPublished(assignment1.name.orEmpty())
+ moduleListPage.assertModuleItemIsPublished(assignment2.name.orEmpty())
+ }
+
+ @Test
+ fun unpublishModulesAndItems() {
+ val data = goToModulesPage(publishedModuleCount = 2, unpublishedModuleCount = 0)
+ val unpublishedModules = data.courseModules.values.first().filter { it.published == true }
+ val assignment1 = data.addAssignment(courseId = data.courses.values.first().id)
+ val assignment2 = data.addAssignment(courseId = data.courses.values.first().id)
+
+ data.addItemToModule(data.courses.values.first(), unpublishedModules[0].id, assignment1, published = true)
+ data.addItemToModule(data.courses.values.first(), unpublishedModules[1].id, assignment2, published = true)
+
+ moduleListPage.refresh()
+
+ openOverflowMenu()
+ moduleListPage.clickOnText(R.string.unpublishAllModulesAndItems)
+ moduleListPage.clickOnText(R.string.unpublishDialogPositiveButton)
+
+ progressPage.clickDone()
+
+ moduleListPage.assertSnackbarText(R.string.allModulesAndAllItemsUnpublished)
+ moduleListPage.assertModuleNotPublished(unpublishedModules[0].name.orEmpty())
+ moduleListPage.assertModuleNotPublished(unpublishedModules[1].name.orEmpty())
+ moduleListPage.assertModuleItemNotPublished(assignment1.name.orEmpty())
+ moduleListPage.assertModuleItemNotPublished(assignment2.name.orEmpty())
+ }
+
+ @Test
+ fun unpublishFileModuleItem() {
+ val data = goToModulesPage()
+ val module = data.courseModules.values.first().first()
+ val course = data.courses.values.first()
+ val fileId = data.addFileToCourse(course.id)
+ val rootFolderId = data.courseRootFolders[course.id]!!.id
+ val fileFolder = data.folderFiles[rootFolderId]?.find { it.id == fileId }
+ data.addItemToModule(
+ course = course,
+ moduleId = module.id,
+ item = fileFolder!!,
+ contentId = fileId,
+ published = true,
+ moduleContentDetails = ModuleContentDetails(
+ hidden = false,
+ locked = false
+ )
+ )
+
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(fileFolder.displayName.orEmpty())
+
+ updateFilePermissionsPage.swipeUpBottomSheet()
+ updateFilePermissionsPage.clickUnpublishRadioButton()
+ updateFilePermissionsPage.clickSaveButton()
+
+ moduleListPage.assertModuleItemNotPublished(fileFolder.displayName.orEmpty())
+ }
+
+ @Test
+ fun publishFileModuleItem() {
+ val data = goToModulesPage()
+ val module = data.courseModules.values.first().first()
+ val course = data.courses.values.first()
+ val fileId = data.addFileToCourse(course.id)
+ val rootFolderId = data.courseRootFolders[course.id]!!.id
+ val fileFolder = data.folderFiles[rootFolderId]?.find { it.id == fileId }
+ data.addItemToModule(
+ course = course,
+ moduleId = module.id,
+ item = fileFolder!!,
+ contentId = fileId,
+ published = false,
+ moduleContentDetails = ModuleContentDetails(
+ hidden = false,
+ locked = true
+ )
+ )
+
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(fileFolder.displayName.orEmpty())
+
+ updateFilePermissionsPage.swipeUpBottomSheet()
+ updateFilePermissionsPage.clickPublishRadioButton()
+ updateFilePermissionsPage.clickSaveButton()
+
+ moduleListPage.assertModuleItemIsPublished(fileFolder.displayName.orEmpty())
+ }
+
+ @Test
+ fun hideFileModuleItem() {
+ val data = goToModulesPage()
+ val module = data.courseModules.values.first().first()
+ val course = data.courses.values.first()
+ val fileId = data.addFileToCourse(course.id)
+ val rootFolderId = data.courseRootFolders[course.id]!!.id
+ val fileFolder = data.folderFiles[rootFolderId]?.find { it.id == fileId }
+ data.addItemToModule(
+ course = course,
+ moduleId = module.id,
+ item = fileFolder!!,
+ contentId = fileId,
+ published = true,
+ moduleContentDetails = ModuleContentDetails(
+ hidden = false,
+ locked = false
+ )
+ )
+
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(fileFolder.displayName.orEmpty())
+
+ updateFilePermissionsPage.swipeUpBottomSheet()
+ updateFilePermissionsPage.clickHideRadioButton()
+ updateFilePermissionsPage.clickSaveButton()
+
+ moduleListPage.assertModuleItemHidden(fileFolder.displayName.orEmpty())
+ }
+
+ @Test
+ fun assertModuleItemDisabled() {
+ val data = goToModulesPage()
+ val module = data.courseModules.values.first().first()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(courseId = data.courses.values.first().id)
+ data.addItemToModule(
+ course = course,
+ moduleId = module.id,
+ item = assignment,
+ published = true,
+ moduleContentDetails = ModuleContentDetails(
+ hidden = false,
+ locked = true
+ ),
+ unpublishable = false
+ )
+
+ moduleListPage.refresh()
+
+ moduleListPage.clickItemOverflow(assignment.name.orEmpty())
+ moduleListPage.assertSnackbarContainsText(assignment.name.orEmpty())
+ }
+
+
+ private fun goToModulesPage(publishedModuleCount: Int = 1, unpublishedModuleCount: Int = 0): MockCanvas {
+ val data = MockCanvas.init(teacherCount = 1, courseCount = 1, favoriteCourseCount = 1)
+ val course = data.courses.values.first()
+
+ data.addCoursePermissions(
+ course.id,
+ CanvasContextPermission() // Just need to have some sort of permissions object registered
+ )
+
+ val modulesTab = Tab(position = 2, label = "Modules", visibility = "public", tabId = Tab.MODULES_ID)
+ data.courseTabs[course.id]!! += modulesTab
+
+ repeat(publishedModuleCount) { data.addModuleToCourse(course, Randomizer.randomModuleName(), published = true) }
+ repeat(unpublishedModuleCount) {
+ data.addModuleToCourse(
+ course,
+ Randomizer.randomModuleName(),
+ published = false
+ )
+ }
+
+ val teacher = data.teachers.first()
+ val token = data.tokenFor(teacher)!!
+ tokenLogin(data.domain, token, teacher)
+
+ dashboardPage.openCourse(course)
+ courseBrowserPage.openModulesTab()
+ return data
+ }
+
+}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizDetailsPageTest.kt
index 859e63e851..d6177861e7 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizDetailsPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizDetailsPageTest.kt
@@ -15,14 +15,17 @@
*/
package com.instructure.teacher.ui
-import com.instructure.canvas.espresso.mockCanvas.*
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
+import com.instructure.canvas.espresso.mockCanvas.addQuizSubmission
+import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse
+import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.CanvasContextPermission
import com.instructure.canvasapi2.models.Quiz
import com.instructure.dataseeding.util.ago
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.espresso.TestRail
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -32,42 +35,36 @@ import org.junit.Test
class QuizDetailsPageTest: TeacherTest() {
@Test
- @TestRail(ID = "C3109579")
override fun displaysPageObjects() {
getToQuizDetailsPage()
quizDetailsPage.assertPageObjects()
}
@Test
- @TestRail(ID = "C3109579")
fun displaysCorrectDetails() {
val quiz = getToQuizDetailsPage()
quizDetailsPage.assertQuizDetails(quiz)
}
@Test
- @TestRail(ID = "C3109579")
fun displaysInstructions() {
getToQuizDetailsPage(withDescription = true)
quizDetailsPage.assertDisplaysInstructions()
}
@Test
- @TestRail(ID = "C3134480")
fun displaysNoInstructionsMessage() {
getToQuizDetailsPage()
quizDetailsPage.assertDisplaysNoInstructionsView()
}
@Test
- @TestRail(ID = "C3134481")
fun displaysClosedAvailability() {
getToQuizDetailsPage(lockAt = 1.days.ago.iso8601)
quizDetailsPage.assertQuizClosed()
}
@Test
- @TestRail(ID = "C3134482")
fun displaysNoFromDate() {
val lockAt = 2.days.fromNow.iso8601
getToQuizDetailsPage(lockAt = lockAt)
@@ -75,7 +72,6 @@ class QuizDetailsPageTest: TeacherTest() {
}
@Test
- @TestRail(ID = "C3134483")
fun displaysNoToDate() {
getToQuizDetailsPage(unlockAt = 2.days.ago.iso8601)
quizDetailsPage.assertFromFilledAndToEmpty()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/UpdateFilePermissionsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/UpdateFilePermissionsPageTest.kt
new file mode 100644
index 0000000000..3fb6f84c2e
--- /dev/null
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/UpdateFilePermissionsPageTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.instructure.teacher.ui
+
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
+import com.instructure.canvas.espresso.mockCanvas.addFileToCourse
+import com.instructure.canvas.espresso.mockCanvas.addItemToModule
+import com.instructure.canvas.espresso.mockCanvas.addModuleToCourse
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.CanvasContextPermission
+import com.instructure.canvasapi2.models.ModuleContentDetails
+import com.instructure.canvasapi2.models.Tab
+import com.instructure.canvasapi2.utils.toApiString
+import com.instructure.dataseeding.util.Randomizer
+import com.instructure.teacher.ui.utils.TeacherTest
+import com.instructure.teacher.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Test
+import java.util.Calendar
+import java.util.Date
+
+@HiltAndroidTest
+class UpdateFilePermissionsPageTest : TeacherTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ @Test
+ fun assertFilePublished() {
+ goToPage(fileAvailability = "published")
+ updateFilePermissionsPage.assertFilePublished()
+ }
+
+ @Test
+ fun assertFileUnpublished() {
+ goToPage(fileAvailability = "unpublished")
+ updateFilePermissionsPage.assertFileUnpublished()
+ }
+
+ @Test
+ fun assertFileHidden() {
+ goToPage(fileAvailability = "hidden")
+ updateFilePermissionsPage.assertFileHidden()
+ }
+
+ @Test
+ fun assertFileScheduled() {
+ val calendar = Calendar.getInstance()
+ val unlockDate = calendar.time
+ val lockDate = calendar.apply { add(Calendar.MONTH, 1) }.time
+ goToPage(fileAvailability = "scheduled", unlockDate = unlockDate, lockDate = lockDate)
+ updateFilePermissionsPage.assertFileScheduled()
+ }
+
+ @Test
+ fun assertFileVisibilityInherit() {
+ goToPage(fileVisibility = "inherit", fileAvailability = "published")
+ updateFilePermissionsPage.assertFileVisibilityInherit()
+ }
+
+ @Test
+ fun assertFileVisibilityContext() {
+ goToPage(fileVisibility = "context", fileAvailability = "published")
+ updateFilePermissionsPage.assertFileVisibilityContext()
+ }
+
+ @Test
+ fun assertFileVisibilityInstitution() {
+ goToPage(fileVisibility = "institution", fileAvailability = "published")
+ updateFilePermissionsPage.assertFileVisibilityInstitution()
+ }
+
+ @Test
+ fun assertFileVisibilityPublic() {
+ goToPage(fileVisibility = "public", fileAvailability = "published")
+ updateFilePermissionsPage.assertFileVisibilityPublic()
+ }
+
+ @Test
+ fun assertScheduleLayoutVisible() {
+ val calendar = Calendar.getInstance()
+ val unlockDate = calendar.time
+ val lockDate = calendar.apply { add(Calendar.MONTH, 1) }.time
+ goToPage(fileAvailability = "scheduled", unlockDate = unlockDate, lockDate = lockDate)
+ updateFilePermissionsPage.assertScheduleLayoutDisplayed()
+ }
+
+ @Test
+ fun assertScheduleLayoutNotVisible() {
+ goToPage(fileAvailability = "published")
+ updateFilePermissionsPage.assertScheduleLayoutNotDisplayed()
+ }
+
+ @Test
+ fun assertUnlockDate() {
+ val calendar = Calendar.getInstance()
+ val unlockDate = calendar.time
+ val lockDate = calendar.apply { add(Calendar.MONTH, 1) }.time
+ goToPage(fileAvailability = "scheduled", unlockDate = unlockDate, lockDate = lockDate)
+ updateFilePermissionsPage.assertUnlockDate(unlockDate)
+ }
+
+ @Test
+ fun assertLockDate() {
+ val calendar = Calendar.getInstance()
+ val unlockDate = calendar.time
+ val lockDate = calendar.apply { add(Calendar.MONTH, 1) }.time
+ goToPage(fileAvailability = "scheduled", unlockDate = unlockDate, lockDate = lockDate)
+ updateFilePermissionsPage.assertLockDate(lockDate)
+ }
+
+ @Test
+ fun assertVisibilityDisabledIfUnpublished() {
+ goToPage(fileVisibility = "public", fileAvailability = "unpublished")
+ updateFilePermissionsPage.assertVisibilityDisabled()
+ }
+
+ @Test
+ fun assertVisibilityEnabled() {
+ goToPage(fileVisibility = "public", fileAvailability = "published")
+ updateFilePermissionsPage.assertVisibilityEnabled()
+ }
+
+ private fun goToPage(fileVisibility: String = "inherit", fileAvailability: String = "published", unlockDate: Date? = null, lockDate: Date? = null) : MockCanvas {
+ val data = MockCanvas.init(teacherCount = 1, courseCount = 1, favoriteCourseCount = 1)
+ val course = data.courses.values.first()
+
+ data.addCoursePermissions(
+ course.id,
+ CanvasContextPermission() // Just need to have some sort of permissions object registered
+ )
+
+ val modulesTab = Tab(position = 2, label = "Modules", visibility = "public", tabId = Tab.MODULES_ID)
+ data.courseTabs[course.id]!! += modulesTab
+
+ data.addModuleToCourse(course, Randomizer.randomModuleName(), published = true)
+
+ val fileId = data.addFileToCourse(course.id, visibilityLevel = fileVisibility)
+ val rootFolderId = data.courseRootFolders[course.id]!!.id
+ val fileFolder = data.folderFiles[rootFolderId]?.find { it.id == fileId }
+
+ val module = data.courseModules.values.first().first()
+
+ data.addItemToModule(
+ course = course,
+ moduleId = module.id,
+ item = fileFolder!!,
+ contentId = fileId,
+ published = fileAvailability == "published",
+ moduleContentDetails = ModuleContentDetails(
+ hidden = fileAvailability == "hidden",
+ locked = fileAvailability == "unpublished",
+ unlockAt = unlockDate?.toApiString(),
+ lockAt = lockDate?.toApiString()
+ )
+ )
+
+ val teacher = data.teachers.first()
+ val token = data.tokenFor(teacher)!!
+ tokenLogin(data.domain, token, teacher)
+
+ dashboardPage.openCourse(course)
+ courseBrowserPage.openModulesTab()
+ moduleListPage.clickItemOverflow(fileFolder.name.orEmpty())
+ updateFilePermissionsPage.swipeUpBottomSheet()
+ return data
+ }
+}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt
index 93d4fe5944..d4581c848a 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt
@@ -29,11 +29,7 @@ import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
-/**
- * Announcements e2e test
- *
- * @constructor Create empty Announcements e2e test
- */
+
@HiltAndroidTest
class AnnouncementsE2ETest : TeacherTest() {
@@ -44,10 +40,6 @@ class AnnouncementsE2ETest : TeacherTest() {
//Because of naming conventions, we are using 'announcementDetailsPage' naming in this class to make the code more readable and straightforward.
private val announcementDetailsPage = discussionsDetailsPage
- /**
- * Test announcements e2e
- *
- */
@E2E
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.E2E)
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt
index 49a4aea411..b11e0feab6 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt
@@ -25,10 +25,6 @@ import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.dataseeding.api.AssignmentsApi
import com.instructure.dataseeding.api.SubmissionsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.AttachmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.FileUploadType
import com.instructure.dataseeding.model.GradingType
import com.instructure.dataseeding.model.SubmissionType
@@ -181,8 +177,8 @@ class AssignmentE2ETest : TeacherTest() {
assignmentDetailsPage.assertNotSubmitted(1,3)
assignmentDetailsPage.assertNeedsGrading(2,3)
- Log.d(PREPARATION_TAG,"Grade the previously seeded submission for ${gradedStudent.name} student.")
- gradeSubmission(teacher, course, assignment, gradedStudent)
+ Log.d(PREPARATION_TAG,"Grade the previously seeded submission for '${gradedStudent.name}' student.")
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, assignment[0].id, gradedStudent.id, postedGrade = "15")
Log.d(STEP_TAG,"Refresh the page. Assert that the number of 'Graded' is increased and the number of 'Not Submitted' and 'Needs Grading' are decreased.")
assignmentDetailsPage.refresh()
@@ -306,16 +302,13 @@ class AssignmentE2ETest : TeacherTest() {
dueAt = 1.days.fromNow.iso8601
))
- Log.d(PREPARATION_TAG,"Submit ${assignment.name} assignment for ${student.name} student.")
- SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest(
- assignmentId = assignment.id,
- courseId = course.id,
- studentToken = student.token,
+ Log.d(PREPARATION_TAG,"Submit '${assignment.name}' assignment for '${student.name}' student.")
+ SubmissionsApi.seedAssignmentSubmission(course.id, student.token, assignment.id,
submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(
amount = 1,
submissionType = SubmissionType.ONLINE_TEXT_ENTRY
))
- ))
+ )
Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
tokenLogin(teacher)
@@ -363,7 +356,7 @@ class AssignmentE2ETest : TeacherTest() {
val course = data.coursesList[0]
Log.d(PREPARATION_TAG, "Seed a text assignment/file/submission.")
- val assignment = createAssignment(course, teacher)
+ val assignment = AssignmentsApi.createAssignment(course.id, teacher.token, submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), allowedExtensions = listOf("txt"))
Log.d(PREPARATION_TAG, "Seed a text file.")
val submissionUploadInfo = uploadTextFile(
@@ -373,8 +366,8 @@ class AssignmentE2ETest : TeacherTest() {
fileUploadType = FileUploadType.ASSIGNMENT_SUBMISSION
)
- Log.d(PREPARATION_TAG, "Submit the ${assignment.name} assignment.")
- submitCourseAssignment(course, assignment, submissionUploadInfo, student)
+ Log.d(PREPARATION_TAG, "Submit the '${assignment.name}' assignment.")
+ SubmissionsApi.submitCourseAssignment(course.id, student.token, assignment.id, submissionType = SubmissionType.ONLINE_UPLOAD, fileIds = mutableListOf(submissionUploadInfo.id))
Log.d(PREPARATION_TAG,"Seed a comment attachment upload.")
val commentUploadInfo = uploadTextFile(
@@ -384,7 +377,8 @@ class AssignmentE2ETest : TeacherTest() {
fileUploadType = FileUploadType.COMMENT_ATTACHMENT
)
- commentOnSubmission(student, course, assignment, commentUploadInfo)
+ Log.d(PREPARATION_TAG, "Comment a text file as a teacher to the '${student.name}' student's submission of the '${assignment.name}' assignment.")
+ SubmissionsApi.commentOnSubmission(course.id, student.token, assignment.id, fileIds = mutableListOf(commentUploadInfo.id))
Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
tokenLogin(teacher)
@@ -406,64 +400,4 @@ class AssignmentE2ETest : TeacherTest() {
assignmentSubmissionListPage.assertCommentAttachmentDisplayedCommon(commentUploadInfo.fileName, student.shortName)
}
- private fun gradeSubmission(
- teacher: CanvasUserApiModel,
- course: CourseApiModel,
- assignment: List,
- gradedStudent: CanvasUserApiModel
- ) {
- SubmissionsApi.gradeSubmission(
- teacherToken = teacher.token,
- courseId = course.id,
- assignmentId = assignment[0].id,
- studentId = gradedStudent.id,
- postedGrade = "15",
- excused = false
- )
- }
-
- private fun createAssignment(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = course.id,
- withDescription = false,
- submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD),
- allowedExtensions = listOf("txt"),
- teacherToken = teacher.token
- )
- )
- }
-
- private fun submitCourseAssignment(
- course: CourseApiModel,
- assignment: AssignmentApiModel,
- submissionUploadInfo: AttachmentApiModel,
- student: CanvasUserApiModel
- ) {
- SubmissionsApi.submitCourseAssignment(
- submissionType = SubmissionType.ONLINE_UPLOAD,
- courseId = course.id,
- assignmentId = assignment.id,
- fileIds = mutableListOf(submissionUploadInfo.id),
- studentToken = student.token
- )
- }
-
- private fun commentOnSubmission(
- student: CanvasUserApiModel,
- course: CourseApiModel,
- assignment: AssignmentApiModel,
- commentUploadInfo: AttachmentApiModel
- ) {
- SubmissionsApi.commentOnSubmission(
- studentToken = student.token,
- courseId = course.id,
- assignmentId = assignment.id,
- fileIds = mutableListOf(commentUploadInfo.id)
- )
- }
-
}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CommentLibraryE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CommentLibraryE2ETest.kt
index f44d8e23d7..80870d73a1 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CommentLibraryE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CommentLibraryE2ETest.kt
@@ -27,7 +27,7 @@ import com.instructure.dataseeding.api.CommentLibraryApi
import com.instructure.dataseeding.api.SubmissionsApi
import com.instructure.dataseeding.api.UserApi
import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.GradingType
+import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.model.UserSettingsApiModel
import com.instructure.dataseeding.util.days
@@ -56,8 +56,8 @@ class CommentLibraryE2ETest : TeacherTest() {
val student = data.studentsList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Preparing assignment and submit that with the student. Enable comment library in user settings.")
- val testAssignment = prepareData(course.id, student.token, teacher.token, teacher.id)
+ Log.d(PREPARATION_TAG,"Make an assignment with a submission for the '${course.name}' course and '${student.name}' student. Set the 'Show suggestions when typing' setting to see the comment library itself.")
+ val testAssignment = prepareSettingsAndMakeAssignmentWithSubmission(course, student.token, teacher.token, teacher.id)
Log.d(PREPARATION_TAG,"Generate comments for comment library.")
val testComment = "Test Comment"
@@ -76,7 +76,7 @@ class CommentLibraryE2ETest : TeacherTest() {
speedGraderPage.selectCommentsTab()
val testText = "another"
- Log.d(STEP_TAG,"Type $testText word and check if there is only one matching suggestion visible.")
+ Log.d(STEP_TAG,"Type '$testText' word and check if there is only one matching suggestion visible.")
speedGraderCommentsPage.typeComment(testText)
commentLibraryPage.assertPageObjects()
commentLibraryPage.assertSuggestionsCount(1)
@@ -89,7 +89,7 @@ class CommentLibraryE2ETest : TeacherTest() {
commentLibraryPage.assertSuggestionsCount(2)
val testText2 = "test"
- Log.d(STEP_TAG,"Type $testText2 word and check if there are two matching suggestion visible.")
+ Log.d(STEP_TAG,"Type '$testText2' word and check if there are two matching suggestion visible.")
commentLibraryPage.closeCommentLibrary()
speedGraderCommentsPage.typeComment(testText2)
commentLibraryPage.assertPageObjects()
@@ -117,30 +117,15 @@ class CommentLibraryE2ETest : TeacherTest() {
commentLibraryPage.assertEmptyViewVisible()
}
- private fun prepareData(
- courseId: Long,
+ private fun prepareSettingsAndMakeAssignmentWithSubmission(
+ course: CourseApiModel,
studentToken: String,
teacherToken: String,
teacherId: Long
): AssignmentApiModel {
- val testAssignment = AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = courseId,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- gradingType = GradingType.POINTS,
- teacherToken = teacherToken,
- pointsPossible = 25.0,
- dueAt = 1.days.fromNow.iso8601
- )
- )
- SubmissionsApi.submitCourseAssignment(
- submissionType = SubmissionType.ONLINE_TEXT_ENTRY,
- courseId = courseId,
- assignmentId = testAssignment.id,
- fileIds = emptyList().toMutableList(),
- studentToken = studentToken
- )
+ val testAssignment = AssignmentsApi.createAssignment(course.id, teacherToken, pointsPossible = 25.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY))
+ SubmissionsApi.submitCourseAssignment(course.id, studentToken, testAssignment.id, submissionType = SubmissionType.ONLINE_TEXT_ENTRY)
val request = UserSettingsApiModel(
manualMarkAsRead = false,
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CourseSettingsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CourseSettingsE2ETest.kt
index ba5c4d92bc..3fdd3691c6 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CourseSettingsE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CourseSettingsE2ETest.kt
@@ -48,10 +48,10 @@ class CourseSettingsE2ETest : TeacherTest() {
val firstCourse = data.coursesList[0]
val secondCourse = data.coursesList[1]
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
- Log.d(STEP_TAG, "Open ${firstCourse.name} course and click on Course Settings button.")
+ Log.d(STEP_TAG, "Open '${firstCourse.name}' course and click on Course Settings button.")
dashboardPage.waitForRender()
dashboardPage.openCourse(firstCourse)
courseBrowserPage.clickSettingsButton()
@@ -63,7 +63,7 @@ class CourseSettingsE2ETest : TeacherTest() {
courseSettingsPage.assertHomePageChanged(newCourseHomePage)
val newCourseName = "New Course Name"
- Log.d(STEP_TAG, "Click on 'Course Name' menu and edit course's name to be $newCourseName. Assert that the course's name has been changed.")
+ Log.d(STEP_TAG, "Click on 'Course Name' menu and edit course's name to be '$newCourseName'. Assert that the course's name has been changed.")
courseSettingsPage.clickCourseName()
courseSettingsPage.editCourseName(newCourseName)
courseSettingsPage.assertCourseNameChanged(newCourseName)
@@ -78,7 +78,7 @@ class CourseSettingsE2ETest : TeacherTest() {
dashboardPage.waitForRender()
dashboardPage.assertDisplaysCourse(newCourseName)
- Log.d(STEP_TAG, "Open ${secondCourse.name} course and click on Course Settings button.")
+ Log.d(STEP_TAG, "Open '${secondCourse.name}' course and click on Course Settings button.")
dashboardPage.waitForRender()
dashboardPage.openCourse(secondCourse)
courseBrowserPage.clickSettingsButton()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt
index f0899b4832..c135b476ad 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt
@@ -86,7 +86,7 @@ class DashboardE2ETest : TeacherTest() {
dashboardPage.assertDisplaysCourse(course2)
dashboardPage.assertCourseNotDisplayed(course1)
- Log.d(STEP_TAG,"Opens ${course2.name} course and assert if Course Details Page has been opened. Navigate back to Dashboard Page.")
+ Log.d(STEP_TAG,"Opens '${course2.name}' course and assert if Course Details Page has been opened. Navigate back to Dashboard Page.")
dashboardPage.assertOpensCourse(course2)
Espresso.pressBack()
@@ -146,11 +146,12 @@ class DashboardE2ETest : TeacherTest() {
@Test
@TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.DASHBOARD, TestCategory.E2E)
fun testHelpMenuE2E() {
+
Log.d(PREPARATION_TAG,"Seeding data.")
val data = seedData(teachers = 1, courses = 1)
val teacher = data.teachersList[0]
- Log.d(STEP_TAG,"Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG,"Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt
index b81217bb91..3792c0fc5a 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt
@@ -31,6 +31,7 @@ import org.junit.Test
@HiltAndroidTest
class DiscussionsE2ETest : TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -47,11 +48,11 @@ class DiscussionsE2ETest : TeacherTest() {
val discussion = data.discussionsList[0]
val discussion2 = data.discussionsList[1]
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
- Log.d(STEP_TAG,"Open ${course.name} course.")
+ Log.d(STEP_TAG,"Open '${course.name}' course.")
dashboardPage.openCourse(course.name)
courseBrowserPage.waitForRender()
@@ -90,7 +91,7 @@ class DiscussionsE2ETest : TeacherTest() {
discussionsListPage.assertGroupDisplayed("Pinned")
discussionsListPage.assertDiscussionInGroup("Pinned", discussion2.title)
- Log.d(STEP_TAG, "Assert that both of the discussions, '${discussion.title}' and '${discussion2.title}' discusssions are displayed.")
+ Log.d(STEP_TAG, "Assert that both of the discussions, '${discussion.title}' and '${discussion2.title}' discussions are displayed.")
discussionsListPage.assertHasDiscussion(newTitle)
discussionsListPage.assertHasDiscussion(discussion2)
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt
index 942c32b4da..168d5053f9 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt
@@ -26,18 +26,12 @@ import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvasapi2.managers.DiscussionManager
import com.instructure.canvasapi2.models.CanvasContext
-import com.instructure.canvasapi2.models.DiscussionEntry
import com.instructure.canvasapi2.utils.weave.awaitApiResponse
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryWeave
import com.instructure.dataseeding.api.AssignmentsApi
import com.instructure.dataseeding.api.DiscussionTopicsApi
import com.instructure.dataseeding.api.SubmissionsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.AttachmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
-import com.instructure.dataseeding.model.DiscussionApiModel
import com.instructure.dataseeding.model.FileUploadType
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.Randomizer
@@ -53,6 +47,7 @@ import java.io.FileWriter
@HiltAndroidTest
class FilesE2ETest: TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -69,7 +64,7 @@ class FilesE2ETest: TeacherTest() {
val course = data.coursesList[0]
Log.d(PREPARATION_TAG, "Seed a text assignment/file/submission.")
- val assignment = createAssignment(course, teacher)
+ val assignment = AssignmentsApi.createAssignment(course.id, teacher.token, submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), allowedExtensions = listOf("txt"))
Log.d(PREPARATION_TAG, "Seed a text file.")
val submissionUploadInfo = uploadTextFile(
@@ -79,8 +74,8 @@ class FilesE2ETest: TeacherTest() {
fileUploadType = FileUploadType.ASSIGNMENT_SUBMISSION
)
- Log.d(PREPARATION_TAG, "Submit the ${assignment.name} assignment.")
- submitCourseAssignment(course, assignment, submissionUploadInfo, student)
+ Log.d(PREPARATION_TAG, "Submit the '${assignment.name}' assignment.")
+ SubmissionsApi.submitCourseAssignment(course.id, student.token, assignment.id, submissionType = SubmissionType.ONLINE_UPLOAD, fileIds = mutableListOf(submissionUploadInfo.id))
Log.d(PREPARATION_TAG,"Seed a comment attachment upload.")
val commentUploadInfo = uploadTextFile(
@@ -90,10 +85,11 @@ class FilesE2ETest: TeacherTest() {
fileUploadType = FileUploadType.COMMENT_ATTACHMENT
)
- commentOnSubmission(student, course, assignment, commentUploadInfo)
+ Log.d(PREPARATION_TAG, "Comment a text file as a teacher to the '${student.name}' student's submission of the '${assignment.name}' assignment.")
+ SubmissionsApi.commentOnSubmission(course.id, student.token, assignment.id, fileIds = mutableListOf(commentUploadInfo.id))
Log.d(PREPARATION_TAG,"Seed a discussion topic. Will add a reply with attachment below.")
- val discussionTopic = createDiscussion(course, student)
+ val discussionTopic= DiscussionTopicsApi.createDiscussion(course.id, student.token)
Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
tokenLogin(teacher)
@@ -113,7 +109,7 @@ class FilesE2ETest: TeacherTest() {
Log.d(PREPARATION_TAG,"Use real API (rather than seeding) to create a reply to our discussion that contains an attachment.")
tryWeave {
- awaitApiResponse {
+ awaitApiResponse {
DiscussionManager.postToDiscussionTopic(
canvasContext = CanvasContext.emptyCourseContext(id = course.id),
topicId = discussionTopic.id,
@@ -132,26 +128,26 @@ class FilesE2ETest: TeacherTest() {
Log.d(STEP_TAG,"Assert that there is a directory called 'unfiled' is displayed.")
fileListPage.assertItemDisplayed("unfiled") // Our discussion attachment goes under "unfiled"
- Log.d(STEP_TAG,"Select 'unfiled' directory. Assert that ${discussionAttachmentFile.name} file is displayed on the File List Page.")
+ Log.d(STEP_TAG,"Select 'unfiled' directory. Assert that '${discussionAttachmentFile.name}' file is displayed on the File List Page.")
fileListPage.selectItem("unfiled")
fileListPage.assertItemDisplayed(discussionAttachmentFile.name)
Log.d(STEP_TAG,"Navigate back to the Dashboard Page.")
ViewUtils.pressBackButton(2)
- Log.d(STEP_TAG,"Open ${course.name} course and navigate to Assignments Page.")
+ Log.d(STEP_TAG,"Open '${course.name}' course and navigate to Assignments Page.")
dashboardPage.openCourse(course.name)
courseBrowserPage.openAssignmentsTab()
- Log.d(STEP_TAG,"Click on ${assignment.name} assignment and navigate to Submissions Page.")
+ Log.d(STEP_TAG,"Click on '${assignment.name}' assignment and navigate to Submissions Page.")
assignmentListPage.clickAssignment(assignment)
assignmentDetailsPage.openSubmissionsPage()
- Log.d(STEP_TAG,"Click on ${student.name} student's submission and navigate to Files Tab.")
+ Log.d(STEP_TAG,"Click on '${student.name}' student's submission and navigate to Files Tab.")
assignmentSubmissionListPage.clickSubmission(student)
speedGraderPage.selectFilesTab(1)
- Log.d(STEP_TAG,"Assert that ${submissionUploadInfo.fileName} file. Navigate to Comments Tab and ${commentUploadInfo.fileName} comment attachment is displayed.")
+ Log.d(STEP_TAG,"Assert that '${submissionUploadInfo.fileName}' file. Navigate to Comments Tab and '${commentUploadInfo.fileName}' comment attachment is displayed.")
assignmentSubmissionListPage.assertFileDisplayed(submissionUploadInfo.fileName)
speedGraderPage.selectCommentsTab()
assignmentSubmissionListPage.assertCommentAttachmentDisplayedCommon(commentUploadInfo.fileName, student.shortName)
@@ -178,25 +174,25 @@ class FilesE2ETest: TeacherTest() {
fileListPage.searchable.pressSearchBackButton()
fileListPage.assertFileListCount(1)
- Log.d(STEP_TAG,"Select 'unfiled' directory. Assert that ${discussionAttachmentFile.name} file is displayed on the File List Page.")
+ Log.d(STEP_TAG,"Select 'unfiled' directory. Assert that '${discussionAttachmentFile.name}' file is displayed on the File List Page.")
fileListPage.selectItem("unfiled")
fileListPage.assertItemDisplayed(discussionAttachmentFile.name)
- Log.d(STEP_TAG,"Select ${discussionAttachmentFile.name} file.")
+ Log.d(STEP_TAG,"Select '${discussionAttachmentFile.name}' file.")
fileListPage.selectItem(discussionAttachmentFile.name)
val newFileName = "newFileName.txt"
- Log.d(STEP_TAG,"Rename ${discussionAttachmentFile.name} file to: $newFileName.")
+ Log.d(STEP_TAG,"Rename '${discussionAttachmentFile.name}' file to: '$newFileName'.")
fileListPage.renameFile(newFileName)
Log.d(STEP_TAG,"Navigate back to File List Page.")
Espresso.pressBack()
fileListPage.assertPageObjects()
- Log.d(STEP_TAG,"Assert that the file is displayed with it's new file name: $newFileName.")
+ Log.d(STEP_TAG,"Assert that the file is displayed with it's new file name: '$newFileName'.")
fileListPage.assertItemDisplayed(newFileName)
- Log.d(STEP_TAG,"Delete $newFileName file.")
+ Log.d(STEP_TAG,"Delete '$newFileName' file.")
fileListPage.deleteFile(newFileName)
fileListPage.assertPageObjects()
@@ -209,7 +205,7 @@ class FilesE2ETest: TeacherTest() {
fileListPage.createFolder(newFolderName)
fileListPage.assertItemDisplayed(newFolderName)
- Log.d(STEP_TAG, "Click on 'Search' (magnifying glass) icon and type '${newFolderName}', the file's name to the search input field.")
+ Log.d(STEP_TAG, "Click on 'Search' (magnifying glass) icon and type '$newFolderName', the file's name to the search input field.")
fileListPage.searchable.clickOnSearchButton()
fileListPage.searchable.typeToSearchBar(newFolderName)
@@ -222,58 +218,4 @@ class FilesE2ETest: TeacherTest() {
fileListPage.assertItemNotDisplayed(newFolderName)
}
- private fun createDiscussion(
- course: CourseApiModel,
- student: CanvasUserApiModel
- ): DiscussionApiModel {
- return DiscussionTopicsApi.createDiscussion(
- courseId = course.id,
- token = student.token
- )
- }
-
- private fun commentOnSubmission(
- student: CanvasUserApiModel,
- course: CourseApiModel,
- assignment: AssignmentApiModel,
- commentUploadInfo: AttachmentApiModel
- ) {
- SubmissionsApi.commentOnSubmission(
- studentToken = student.token,
- courseId = course.id,
- assignmentId = assignment.id,
- fileIds = mutableListOf(commentUploadInfo.id)
- )
- }
-
- private fun submitCourseAssignment(
- course: CourseApiModel,
- assignment: AssignmentApiModel,
- submissionUploadInfo: AttachmentApiModel,
- student: CanvasUserApiModel
- ) {
- SubmissionsApi.submitCourseAssignment(
- submissionType = SubmissionType.ONLINE_UPLOAD,
- courseId = course.id,
- assignmentId = assignment.id,
- fileIds = mutableListOf(submissionUploadInfo.id),
- studentToken = student.token
- )
- }
-
- private fun createAssignment(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = course.id,
- withDescription = false,
- submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD),
- allowedExtensions = listOf("txt"),
- teacherToken = teacher.token
- )
- )
- }
-
}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt
index 4c74dc3ff7..2ae6c04d5f 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt
@@ -23,15 +23,16 @@ import org.junit.Test
@HiltAndroidTest
class InboxE2ETest : TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
-
@E2E
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.INBOX, TestCategory.E2E)
fun testInboxMessageComposeReplyAndOptionMenuActionsE2E() {
+
Log.d(PREPARATION_TAG, "Seeding data.")
val data = seedData(students = 2, teachers = 1, courses = 1)
val teacher = data.teachersList[0]
@@ -41,10 +42,10 @@ class InboxE2ETest : TeacherTest() {
val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token)
val group = GroupsApi.createGroup(groupCategory.id, teacher.token)
- Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} student to the group: ${group.name}.")
+ Log.d(PREPARATION_TAG, "Create group membership for '${student1.name}' student to the group: '${group.name}'.")
GroupsApi.createGroupMembership(group.id, student1.id, teacher.token)
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
dashboardPage.assertDisplaysCourse(course)
@@ -80,11 +81,11 @@ class InboxE2ETest : TeacherTest() {
Log.d(STEP_TAG,"Add a new conversation message manually via UI. Click on 'New Message' ('+') button.")
inboxPage.clickAddMessageFAB()
- Log.d(STEP_TAG,"Select ${course.name} from course spinner.Click on the '+' icon next to the recipients input field. Select the two students: ${student1.name} and ${student2.name}. Click on 'Done'.")
+ Log.d(STEP_TAG,"Select '${course.name}' from course spinner. Click on the '+' icon next to the recipients input field. Select the two students: '${student1.name}' and '${student2.name}'. Click on 'Done'.")
addNewMessage(course,data.studentsList)
val subject = "Hello there"
- Log.d(STEP_TAG,"Fill in the 'Subject' field with the value: $subject. Add some message text and click on 'Send' (aka. 'Arrow') button.")
+ Log.d(STEP_TAG,"Fill in the 'Subject' field with the value: '$subject'. Add some message text and click on 'Send' (aka. 'Arrow') button.")
addMessagePage.composeMessageWithSubject(subject, "General Kenobi")
addMessagePage.clickSendButton()
@@ -94,7 +95,7 @@ class InboxE2ETest : TeacherTest() {
Log.d(STEP_TAG,"Assert that the previously sent conversation is displayed.")
inboxPage.assertHasConversation()
- Log.d(STEP_TAG,"Click on $subject conversation.")
+ Log.d(STEP_TAG,"Click on '$subject' conversation.")
inboxPage.clickConversation(subject)
val replyMessageTwo = "Test Reply 2"
@@ -171,10 +172,10 @@ class InboxE2ETest : TeacherTest() {
val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token)
val group = GroupsApi.createGroup(groupCategory.id, teacher.token)
- Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} student to the group: ${group.name}.")
+ Log.d(PREPARATION_TAG, "Create group membership for '${student1.name}' student to the group: '${group.name}'.")
GroupsApi.createGroupMembership(group.id, student1.id, teacher.token)
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
dashboardPage.assertDisplaysCourse(course)
@@ -184,30 +185,17 @@ class InboxE2ETest : TeacherTest() {
inboxPage.assertInboxEmpty()
Log.d(PREPARATION_TAG, "Seed an Inbox conversation via API.")
- val seedConversation = ConversationsApi.createConversation(
- token = student1.token,
- recipients = listOf(teacher.id.toString())
- )
+ val seedConversation = ConversationsApi.createConversation(token = student1.token, recipients = listOf(teacher.id.toString()))
Log.d(STEP_TAG, "Refresh the page. Assert that the conversation displayed as unread.")
inboxPage.refresh()
inboxPage.assertThereIsAnUnreadMessage(true)
Log.d(PREPARATION_TAG, "Seed another Inbox conversation via API.")
- val seedConversation2 = ConversationsApi.createConversation(
- token = student1.token,
- recipients = listOf(teacher.id.toString()),
- subject = "Second conversation",
- body = "Second body"
- )
+ val seedConversation2 = ConversationsApi.createConversation(token = student1.token, recipients = listOf(teacher.id.toString()), subject = "Second conversation", body = "Second body")
Log.d(PREPARATION_TAG, "Seed a third Inbox conversation via API.")
- val seedConversation3 = ConversationsApi.createConversation(
- token = student2.token,
- recipients = listOf(teacher.id.toString()),
- subject = "Third conversation",
- body = "Third body"
- )
+ val seedConversation3 = ConversationsApi.createConversation(token = student2.token, recipients = listOf(teacher.id.toString()), subject = "Third conversation", body = "Third body")
Log.d(STEP_TAG,"Refresh the page. Filter the Inbox by selecting 'Inbox' category from the spinner on Inbox Page. Assert that the '${seedConversation[0]}' conversation is displayed. Assert that the conversation is unread yet.")
inboxPage.refresh()
@@ -234,11 +222,11 @@ class InboxE2ETest : TeacherTest() {
inboxPage.clickUnArchive()
inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject)
- Log.d(STEP_TAG,"Navigate to 'INBOX' scope and assert that ${seedConversation2[0].subject} conversation is displayed.")
+ Log.d(STEP_TAG,"Navigate to 'INBOX' scope and assert that '${seedConversation2[0].subject}' conversation is displayed.")
inboxPage.filterMessageScope("Inbox")
inboxPage.assertConversationDisplayed(seedConversation2[0].subject)
- Log.d(STEP_TAG, "Select both of the conversations (${seedConversation[0].subject} and ${seedConversation2[0].subject} and star them." +
+ Log.d(STEP_TAG, "Select both of the conversations '${seedConversation[0].subject}' and '${seedConversation2[0].subject}' and star them." +
"Assert that both of the has been starred and the selected number of conversations on the toolbar shows 2")
inboxPage.selectConversations(listOf(seedConversation2[0].subject, seedConversation3[0].subject))
inboxPage.assertSelectedConversationNumber("2")
@@ -303,6 +291,7 @@ class InboxE2ETest : TeacherTest() {
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.INBOX, TestCategory.E2E)
fun testInboxSwipeGesturesE2E() {
+
Log.d(PREPARATION_TAG, "Seeding data.")
val data = seedData(students = 2, teachers = 1, courses = 1)
val teacher = data.teachersList[0]
@@ -312,10 +301,10 @@ class InboxE2ETest : TeacherTest() {
val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token)
val group = GroupsApi.createGroup(groupCategory.id, teacher.token)
- Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} student to the group: ${group.name}.")
+ Log.d(PREPARATION_TAG, "Create group membership for '${student1.name}' student to the group: '${group.name}'.")
GroupsApi.createGroupMembership(group.id, student1.id, teacher.token)
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
dashboardPage.assertDisplaysCourse(course)
@@ -325,10 +314,7 @@ class InboxE2ETest : TeacherTest() {
inboxPage.assertInboxEmpty()
Log.d(PREPARATION_TAG, "Seed an Inbox conversation via API.")
- val seedConversation = ConversationsApi.createConversation(
- token = student1.token,
- recipients = listOf(teacher.id.toString())
- )
+ ConversationsApi.createConversation(token = student1.token, recipients = listOf(teacher.id.toString()))
Log.d(STEP_TAG,"Refresh the page. Assert that the previously seeded Inbox conversation is displayed. Assert that the message is unread yet.")
inboxPage.refresh()
@@ -454,10 +440,10 @@ class InboxE2ETest : TeacherTest() {
val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token)
val group = GroupsApi.createGroup(groupCategory.id, teacher.token)
- Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} student to the group: ${group.name}.")
+ Log.d(PREPARATION_TAG, "Create group membership for '${student1.name}' student to the group: '${group.name}'.")
GroupsApi.createGroupMembership(group.id, student1.id, teacher.token)
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
dashboardPage.assertDisplaysCourse(course)
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt
index 9b6ff40f1c..1c7ffa3be7 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt
@@ -33,6 +33,7 @@ import org.junit.Test
@HiltAndroidTest
class LoginE2ETest : TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -48,19 +49,19 @@ class LoginE2ETest : TeacherTest() {
val teacher2 = data.teachersList[1]
val course = data.coursesList[0]
- Log.d(STEP_TAG, "Login with user: ${teacher1.name}, login id: ${teacher1.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher1.name}', login id: '${teacher1.loginId}'.")
loginWithUser(teacher1)
Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
assertSuccessfulLogin(teacher1)
- Log.d(STEP_TAG,"Validate ${teacher1.name} user's role as a Teacher.")
+ Log.d(STEP_TAG,"Validate '${teacher1.name}' user's role as a Teacher.")
validateUserRole(teacher1, course, "Teacher")
- Log.d(STEP_TAG,"Log out with ${teacher1.name} student.")
+ Log.d(STEP_TAG,"Log out with '${teacher1.name}' student.")
leftSideNavigationDrawerPage.logout()
- Log.d(STEP_TAG, "Login with user: ${teacher2.name}, login id: ${teacher2.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher2.name}', login id: '${teacher2.loginId}'.")
loginWithUser(teacher2, true)
Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
@@ -72,7 +73,7 @@ class LoginE2ETest : TeacherTest() {
Log.d(STEP_TAG,"Assert that the previously logins has been displayed.")
loginLandingPage.assertDisplaysPreviousLogins()
- Log.d(STEP_TAG, "Login with user: ${teacher1.name}, login id: ${teacher1.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher1.name}', login id: '${teacher1.loginId}'.")
loginWithUser(teacher1, true)
Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
@@ -84,7 +85,7 @@ class LoginE2ETest : TeacherTest() {
Log.d(STEP_TAG,"Assert that the previously logins has been displayed.")
loginLandingPage.assertDisplaysPreviousLogins()
- Log.d(STEP_TAG,"Login with the previous user, ${teacher2.name}, with one click, by clicking on the user's name on the bottom.")
+ Log.d(STEP_TAG,"Login with the previous user, '${teacher2.name}', with one click, by clicking on the user's name on the bottom.")
loginLandingPage.loginWithPreviousUser(teacher2)
Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
@@ -106,7 +107,7 @@ class LoginE2ETest : TeacherTest() {
val student = data.studentsList[0]
val parent = parentData.parentsList[0]
- Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.")
loginWithUser(student)
Log.d(STEP_TAG,"Assert that the user has been landed on 'Not a teacher?' Page.")
@@ -118,7 +119,7 @@ class LoginE2ETest : TeacherTest() {
Log.d(STEP_TAG,"Assert the Teacher app's Login Landing Page's screen is displayed.")
loginLandingPage.assertPageObjects()
- Log.d(STEP_TAG, "Login with user: ${parent.name}, login id: ${parent.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${parent.name}', login id: '${parent.loginId}'.")
loginWithUser(parent, true)
Log.d(STEP_TAG,"Assert that the user has been landed on 'Not a teacher?' Page.")
@@ -141,16 +142,16 @@ class LoginE2ETest : TeacherTest() {
val teacher1 = data.teachersList[0]
val teacher2 = data.teachersList[1]
- Log.d(STEP_TAG, "Login with user: ${teacher1.name}, login id: ${teacher1.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher1.name}', login id: '${teacher1.loginId}'.")
loginWithUser(teacher1)
Log.d(STEP_TAG, "Assert that the Dashboard Page is the landing page and it is loaded successfully.")
assertSuccessfulLogin(teacher1)
- Log.d(STEP_TAG, "Log out with ${teacher1.name} student.")
+ Log.d(STEP_TAG, "Log out with '${teacher1.name}' student.")
leftSideNavigationDrawerPage.logout()
- Log.d(STEP_TAG, "Login with user: ${teacher2.name}, login id: ${teacher2.loginId}, via the last saved school's button.")
+ Log.d(STEP_TAG, "Login with user: '${teacher2.name}', login id: '${teacher2.loginId}', via the last saved school's button.")
loginWithLastSavedSchool(teacher2)
Log.d(STEP_TAG, "Assert that the Dashboard Page is the landing page and it is loaded successfully.")
@@ -171,13 +172,13 @@ class LoginE2ETest : TeacherTest() {
Log.d(STEP_TAG, "Click 'Find My School' button.")
loginLandingPage.clickFindMySchoolButton()
- Log.d(STEP_TAG,"Enter domain: $DOMAIN.instructure.com.")
+ Log.d(STEP_TAG,"Enter domain: '$DOMAIN.instructure.com'.")
loginFindSchoolPage.enterDomain(DOMAIN)
Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.")
loginFindSchoolPage.clickToolbarNextMenuItem()
- Log.d(STEP_TAG, "Try to login with invalid, non-existing credentials ($INVALID_USERNAME, $INVALID_PASSWORD)." +
+ Log.d(STEP_TAG, "Try to login with invalid, non-existing credentials: '$INVALID_USERNAME', '$INVALID_PASSWORD'." +
"Assert that the invalid credentials error message is displayed.")
loginSignInPage.loginAs(INVALID_USERNAME, INVALID_PASSWORD)
loginSignInPage.assertLoginErrorMessage(INVALID_CREDENTIALS_ERROR_MESSAGE)
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt
index 900c775687..b0aaaa1a5a 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt
@@ -2,7 +2,6 @@ package com.instructure.teacher.ui.e2e
import android.util.Log
import androidx.test.espresso.Espresso
-import androidx.test.espresso.web.webdriver.Locator
import com.instructure.canvas.espresso.E2E
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.Priority
@@ -13,26 +12,22 @@ import com.instructure.dataseeding.api.DiscussionTopicsApi
import com.instructure.dataseeding.api.ModulesApi
import com.instructure.dataseeding.api.PagesApi
import com.instructure.dataseeding.api.QuizzesApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
-import com.instructure.dataseeding.model.ModuleApiModel
import com.instructure.dataseeding.model.ModuleItemTypes
-import com.instructure.dataseeding.model.PageApiModel
-import com.instructure.dataseeding.model.QuizApiModel
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.teacher.ui.pages.WebViewTextCheck
-import com.instructure.teacher.ui.utils.TeacherTest
+import com.instructure.teacher.R
+import com.instructure.teacher.ui.utils.TeacherComposeTest
+import com.instructure.teacher.ui.utils.openOverflowMenu
import com.instructure.teacher.ui.utils.seedData
import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
@HiltAndroidTest
-class ModulesE2ETest : TeacherTest() {
+class ModulesE2ETest : TeacherComposeTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -47,205 +42,434 @@ class ModulesE2ETest : TeacherTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}. Assert that ${course.name} course is displayed on the Dashboard.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'. Assert that '${course.name}' course is displayed on the Dashboard.")
tokenLogin(teacher)
dashboardPage.waitForRender()
dashboardPage.assertDisplaysCourse(course)
- Log.d(STEP_TAG,"Open ${course.name} course and navigate to Modules Page.")
+ Log.d(STEP_TAG,"Open '${course.name}' course and navigate to Modules Page.")
dashboardPage.openCourse(course.name)
courseBrowserPage.openModulesTab()
Log.d(STEP_TAG,"Assert that empty view is displayed because there is no Module within the course.")
- modulesPage.assertEmptyView()
-
- Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.")
- val assignment = createAssignment(course, teacher)
-
- Log.d(PREPARATION_TAG,"Seeding quiz for ${course.name} course.")
- val quiz = createQuiz(course, teacher)
-
- Log.d(PREPARATION_TAG,"Create an unpublished page for course: ${course.name}.")
- val testPage = createCoursePage(course, teacher, published = false, frontPage = false, body = "")
-
- Log.d(PREPARATION_TAG,"Create a discussion topic for ${course.name} course.")
- val discussionTopic = createDiscussion(course, teacher)
-
- Log.d(PREPARATION_TAG,"Seeding a module for ${course.name} course. It starts as unpublished.")
- val module = createModule(course, teacher)
-
- Log.d(PREPARATION_TAG,"Associate ${assignment.name} assignment (and the quiz within it) with module: ${module.id}.")
- createModuleItem(course, module, teacher, assignment.name, ModuleItemTypes.ASSIGNMENT.stringVal, assignment.id.toString())
- createModuleItem(course, module, teacher, quiz.title, ModuleItemTypes.QUIZ.stringVal, quiz.id.toString())
-
- Log.d(PREPARATION_TAG,"Associate ${testPage.title} page with module: ${module.id}.")
- createModuleItem(course, module, teacher, testPage.title, ModuleItemTypes.PAGE.stringVal, null, pageUrl = testPage.url)
-
- Log.d(PREPARATION_TAG,"Associate ${discussionTopic.title} discussion with module: ${module.id}.")
- createModuleItem(course, module, teacher, discussionTopic.title, ModuleItemTypes.DISCUSSION.stringVal, discussionTopic.id.toString())
-
- Log.d(STEP_TAG,"Refresh the page. Assert that ${module.name} module is displayed and it is unpublished by default.")
- modulesPage.refresh()
- modulesPage.assertModuleIsDisplayed(module.name)
- modulesPage.assertModuleNotPublished()
-
- Log.d(STEP_TAG,"Assert that ${testPage.title} page is present as a module item, but it's not published.")
- modulesPage.assertModuleItemIsDisplayed(testPage.title)
- modulesPage.assertModuleItemNotPublished(module.name, testPage.title)
-
- Log.d(PREPARATION_TAG,"Publish ${module.name} module via API.")
- ModulesApi.updateModule(
- courseId = course.id,
- id = module.id,
- published = true,
- teacherToken = teacher.token
- )
-
- Log.d(STEP_TAG,"Refresh the page. Assert that ${module.name} module is displayed and it is published.")
- modulesPage.refresh()
- modulesPage.assertModuleIsDisplayed(module.name)
- modulesPage.assertModuleIsPublished()
-
- Log.d(STEP_TAG,"Assert that ${assignment.name} assignment and ${quiz.title} quiz are present as module items, and they are published since their module is published.")
- modulesPage.assertModuleItemIsDisplayed(assignment.name)
- modulesPage.assertModuleItemIsPublished(assignment.name)
- modulesPage.assertModuleItemIsDisplayed(quiz.title)
- modulesPage.assertModuleItemIsPublished(quiz.title)
-
- Log.d(STEP_TAG,"Assert that ${testPage.title} page is present as a module item, but it's not published.")
- modulesPage.assertModuleItemIsDisplayed(testPage.title)
- modulesPage.assertModuleItemIsPublished(testPage.title)
-
- Log.d(STEP_TAG, "Collapse the ${module.name} and assert that the module items has not displayed.")
- modulesPage.clickOnCollapseExpandIcon()
- modulesPage.assertItemCountInModule(module.name, 0)
-
- Log.d(STEP_TAG, "Expand the ${module.name} and assert that the module items are displayed.")
- modulesPage.clickOnCollapseExpandIcon()
- modulesPage.assertItemCountInModule(module.name, 4)
-
- Log.d(PREPARATION_TAG,"Unpublish ${module.name} module via API.")
- ModulesApi.updateModule(
- courseId = course.id,
- id = module.id,
- published = false,
- teacherToken = teacher.token
- )
-
- Log.d(STEP_TAG, "Refresh the Modules Page.")
- modulesPage.refresh()
-
- Log.d(STEP_TAG,"Assert that ${assignment.name} assignment and ${quiz.title} quiz and ${testPage.title} page are present as module items, and they are NOT published since their module is unpublished.")
- modulesPage.assertModuleItemIsDisplayed(assignment.name)
- modulesPage.assertModuleItemNotPublished(module.name, assignment.name)
- modulesPage.assertModuleItemIsDisplayed(quiz.title)
- modulesPage.assertModuleItemNotPublished(module.name, quiz.title)
- modulesPage.assertModuleItemIsDisplayed(testPage.title)
- modulesPage.assertModuleItemNotPublished(module.name, testPage.title)
-
- Log.d(STEP_TAG, "Open the ${assignment.name} assignment module item and assert that the Assignment Details Page is displayed. Navigate back to Modules Page.")
- modulesPage.clickOnModuleItem(assignment.name)
+ moduleListPage.assertEmptyView()
+
+ Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.")
+ val assignment = AssignmentsApi.createAssignment(course.id, teacher.token, withDescription = true, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), dueAt = 1.days.fromNow.iso8601)
+
+ Log.d(PREPARATION_TAG,"Seeding quiz for '${course.name}' course.")
+ val quiz = QuizzesApi.createQuiz(course.id, teacher.token, withDescription = true, dueAt = 3.days.fromNow.iso8601)
+
+ Log.d(PREPARATION_TAG,"Create an unpublished page for course: '${course.name}'.")
+ val testPage = PagesApi.createCoursePage(course.id, teacher.token, published = false, body = "")
+
+ Log.d(PREPARATION_TAG,"Create a discussion topic for '${course.name}' course.")
+ val discussionTopic = DiscussionTopicsApi.createDiscussion(courseId = course.id, token = teacher.token)
+
+ Log.d(PREPARATION_TAG,"Seeding a module for '${course.name}' course. It starts as unpublished.")
+ val module = ModulesApi.createModule(course.id, teacher.token)
+
+ Log.d(PREPARATION_TAG,"Associate '${assignment.name}' assignment with module: '${module.id}'.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module.id, moduleItemTitle = assignment.name, moduleItemType = ModuleItemTypes.ASSIGNMENT.stringVal, contentId = assignment.id.toString())
+
+ Log.d(PREPARATION_TAG,"Associate '${quiz.title}' quiz with module: '${module.id}'.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module.id, moduleItemTitle = quiz.title, moduleItemType = ModuleItemTypes.QUIZ.stringVal, contentId = quiz.id.toString())
+
+ Log.d(PREPARATION_TAG,"Associate '${testPage.title}' page with module: '${module.id}'.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module.id, moduleItemTitle = testPage.title, moduleItemType = ModuleItemTypes.PAGE.stringVal, contentId = null, pageUrl = testPage.url)
+
+ Log.d(PREPARATION_TAG,"Associate '${discussionTopic.title}' discussion with module: '${module.id}'.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module.id, moduleItemTitle = discussionTopic.title, moduleItemType = ModuleItemTypes.DISCUSSION.stringVal, contentId = discussionTopic.id.toString())
+
+ Log.d(STEP_TAG,"Refresh the page. Assert that '${module.name}' module is displayed and it is unpublished by default.")
+ moduleListPage.refresh()
+ moduleListPage.assertModuleIsDisplayed(module.name)
+ moduleListPage.assertModuleNotPublished(module.name)
+
+ Log.d(STEP_TAG,"Assert that '${testPage.title}' page is present as a module item, but it's not published.")
+ moduleListPage.assertModuleItemIsDisplayed(testPage.title)
+ moduleListPage.assertModuleItemNotPublished(testPage.title)
+
+ Log.d(PREPARATION_TAG,"Publish '${module.name}' module via API.")
+ ModulesApi.updateModule(courseId = course.id, moduleId = module.id, published = true, teacherToken = teacher.token)
+
+ Log.d(STEP_TAG,"Refresh the page. Assert that '${module.name}' module is displayed and it is published.")
+ moduleListPage.refresh()
+ moduleListPage.assertModuleIsDisplayed(module.name)
+ moduleListPage.assertModuleIsPublished()
+
+ Log.d(STEP_TAG,"Assert that '${assignment.name}' assignment and '${quiz.title}' quiz are present as module items, and they are published since their module is published.")
+ moduleListPage.assertModuleItemIsDisplayed(assignment.name)
+ moduleListPage.assertModuleItemIsPublished(assignment.name)
+ moduleListPage.assertModuleItemIsDisplayed(quiz.title)
+ moduleListPage.assertModuleItemIsPublished(quiz.title)
+
+ Log.d(STEP_TAG,"Assert that '${testPage.title}' page is present as a module item, but it's not published.")
+ moduleListPage.assertModuleItemIsDisplayed(testPage.title)
+ moduleListPage.assertModuleItemIsPublished(testPage.title)
+
+ Log.d(STEP_TAG, "Collapse the '${module.name}' and assert that the module items has not displayed.")
+ moduleListPage.clickOnCollapseExpandIcon()
+ moduleListPage.assertItemCountInModule(module.name, 0)
+
+ Log.d(STEP_TAG, "Expand the '${module.name}' and assert that the module items are displayed.")
+ moduleListPage.clickOnCollapseExpandIcon()
+ moduleListPage.assertItemCountInModule(module.name, 4)
+
+ Log.d(STEP_TAG, "Open the '${assignment.name}' assignment module item and assert that the Assignment Details Page is displayed. Assert that the module name is displayed at the bottom.")
+ moduleListPage.clickOnModuleItem(assignment.name)
assignmentDetailsPage.assertPageObjects()
+ assignmentDetailsPage.assertAssignmentDetails(assignment)
+ assignmentDetailsPage.moduleItemInteractions.assertModuleNameDisplayed(module.name)
+
+ Log.d(STEP_TAG, "Assert that the previous arrow button is not displayed because the user is on the first assignment's details page, but the next arrow button is displayed.")
+ assignmentDetailsPage.moduleItemInteractions.assertPreviousArrowNotDisplayed()
+ assignmentDetailsPage.moduleItemInteractions.assertNextArrowDisplayed()
+
+ Log.d(STEP_TAG, "Click on the next arrow button and assert that the '${quiz.title}' quiz module item's details page is displayed. Assert that the module name is displayed at the bottom.")
+ assignmentDetailsPage.moduleItemInteractions.clickOnNextArrow()
+ quizDetailsPage.assertQuizDetails(quiz)
+ quizDetailsPage.moduleItemInteractions.assertModuleNameDisplayed(module.name)
+
+ Log.d(STEP_TAG, "Assert that both the previous and next arrow buttons are displayed.")
+ quizDetailsPage.moduleItemInteractions.assertPreviousArrowDisplayed()
+ quizDetailsPage.moduleItemInteractions.assertNextArrowDisplayed()
+
+ Log.d(STEP_TAG, "Click on the next arrow button and assert that the '${testPage.title}' page module item's details page is displayed. Assert that the module name is displayed at the bottom.")
+ quizDetailsPage.moduleItemInteractions.clickOnNextArrow()
+ editPageDetailsPage.assertPageDetails(testPage)
+ editPageDetailsPage.moduleItemInteractions.assertModuleNameDisplayed(module.name)
+
+ Log.d(STEP_TAG, "Assert that both the previous and next arrow buttons are displayed.")
+ editPageDetailsPage.moduleItemInteractions.assertPreviousArrowDisplayed()
+ editPageDetailsPage.moduleItemInteractions.assertNextArrowDisplayed()
+
+ Log.d(STEP_TAG, "Click on the next arrow button and assert that the '${discussionTopic.title}' discussion module item's details page is displayed. Assert that the module name is displayed at the bottom.")
+ editPageDetailsPage.moduleItemInteractions.clickOnNextArrow()
+ discussionsDetailsPage.assertDiscussionTitle(discussionTopic.title)
+ discussionsDetailsPage.assertDiscussionPublished()
+ discussionsDetailsPage.moduleItemInteractions.assertModuleNameDisplayed(module.name)
+
+ Log.d(STEP_TAG, "Assert that the next arrow button is not displayed because the user is on the last assignment's details page, but the previous arrow button is displayed.")
+ discussionsDetailsPage.moduleItemInteractions.assertPreviousArrowDisplayed()
+ discussionsDetailsPage.moduleItemInteractions.assertNextArrowNotDisplayed()
+
+ Log.d(STEP_TAG, "Click on the previous arrow button and assert that the '${testPage.title}' page module item's details page is displayed. Assert that the module name is displayed at the bottom.")
+ quizDetailsPage.moduleItemInteractions.clickOnPreviousArrow()
+ editPageDetailsPage.assertPageDetails(testPage)
+ editPageDetailsPage.moduleItemInteractions.assertModuleNameDisplayed(module.name)
+
+ Log.d(STEP_TAG, "Navigate back to Module List Page.")
Espresso.pressBack()
- Log.d(STEP_TAG, "Open the ${quiz.title} quiz module item and assert that the Quiz Details Page is displayed. Navigate back to Modules Page.")
- modulesPage.clickOnModuleItem(quiz.title)
- quizDetailsPage.assertPageObjects()
- Espresso.pressBack()
+ Log.d(PREPARATION_TAG,"Unpublish '${module.name}' module via API.")
+ ModulesApi.updateModule(courseId = course.id, moduleId = module.id, published = false, teacherToken = teacher.token)
- Log.d(STEP_TAG, "Open the ${testPage.title} page module item and assert that the Page Details Page is displayed. Navigate back to Modules Page.")
- modulesPage.clickOnModuleItem(testPage.title)
- editPageDetailsPage.runTextChecks(WebViewTextCheck(Locator.ID, "header1", "Test Page Text"))
- Espresso.pressBack()
+ Log.d(STEP_TAG, "Refresh the Module List Page.")
+ moduleListPage.refresh()
- Log.d(STEP_TAG, "Open the ${discussionTopic.title} discussion module item and assert that the Discussion Details Page is displayed.")
- modulesPage.clickOnModuleItem(discussionTopic.title)
- discussionsDetailsPage.assertPageObjects()
- }
+ Log.d(STEP_TAG,"Assert that '${assignment.name}' assignment and '${quiz.title}' quiz and '${testPage.title}' page are present as module items, and they are NOT published since their module is unpublished.")
+ moduleListPage.assertModuleItemIsDisplayed(assignment.name)
+ moduleListPage.assertModuleItemNotPublished(assignment.name)
+ moduleListPage.assertModuleItemIsDisplayed(quiz.title)
+ moduleListPage.assertModuleItemNotPublished(quiz.title)
+ moduleListPage.assertModuleItemIsDisplayed(testPage.title)
+ moduleListPage.assertModuleItemNotPublished(testPage.title)
- private fun createModuleItem(
- course: CourseApiModel,
- module: ModuleApiModel,
- teacher: CanvasUserApiModel,
- title: String,
- moduleItemType: String,
- contentId: String?,
- pageUrl: String? = null
- ) {
- ModulesApi.createModuleItem(
- courseId = course.id,
- moduleId = module.id,
- teacherToken = teacher.token,
- title = title,
- type = moduleItemType,
- contentId = contentId,
- pageUrl = pageUrl
- )
- }
+ Log.d(STEP_TAG, "Open the '${assignment.name}' assignment module item and assert that the Assignment Details Page is displayed")
+ moduleListPage.clickOnModuleItem(assignment.name)
- private fun createModule(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ): ModuleApiModel {
- return ModulesApi.createModule(
- courseId = course.id,
- teacherToken = teacher.token,
- unlockAt = null
- )
- }
+ Log.d(STEP_TAG, "Assert that the published status of the '${assignment.name}' assignment became 'Unpublished' on the Assignment Details Page.")
+ assignmentDetailsPage.assertPublishedStatus(false)
- private fun createQuiz(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ): QuizApiModel {
- return QuizzesApi.createQuiz(
- QuizzesApi.CreateQuizRequest(
- courseId = course.id,
- withDescription = true,
- dueAt = 3.days.fromNow.iso8601,
- token = teacher.token,
- published = true
- )
- )
- }
+ Log.d(STEP_TAG, "Open Edit Page of '${assignment.name}' assignment and publish it. Save the change.")
+ assignmentDetailsPage.openEditPage()
+ editAssignmentDetailsPage.clickPublishSwitch()
+ editAssignmentDetailsPage.saveAssignment()
- private fun createAssignment(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ): AssignmentApiModel {
- return AssignmentsApi.createAssignment(
- AssignmentsApi.CreateAssignmentRequest(
- courseId = course.id,
- withDescription = true,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- teacherToken = teacher.token,
- dueAt = 1.days.fromNow.iso8601
- )
- )
- }
+ Log.d(STEP_TAG, "Assert that the published status of the '${assignment.name}' assignment became 'Published' on the Assignment Details Page (as well).")
+ assignmentDetailsPage.assertPublishedStatus(true)
- private fun createCoursePage(
- course: CourseApiModel,
- teacher: CanvasUserApiModel,
- published: Boolean = true,
- frontPage: Boolean = false,
- body: String = EMPTY_STRING
- ): PageApiModel {
- return PagesApi.createCoursePage(
- courseId = course.id,
- published = published,
- frontPage = frontPage,
- token = teacher.token,
- body = body
- )
+ Log.d(STEP_TAG, "Navigate back to Module List Page and assert that the '${assignment.name}' assignment module item's status became 'Published'.")
+ Espresso.pressBack()
+ moduleListPage.assertModuleItemIsPublished(assignment.name)
}
- private fun createDiscussion(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) = DiscussionTopicsApi.createDiscussion(
- courseId = course.id,
- token = teacher.token
- )
+ @E2E
+ @Test
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.E2E)
+ fun testBulkUpdateModulesE2E() {
+
+ Log.d(PREPARATION_TAG, "Seeding data.")
+ val data = seedData(students = 1, teachers = 1, courses = 1)
+ val teacher = data.teachersList[0]
+ val course = data.coursesList[0]
+
+ Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.")
+ val assignment = AssignmentsApi.createAssignment(course.id, teacher.token, withDescription = true, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), dueAt = 1.days.fromNow.iso8601)
+
+ Log.d(PREPARATION_TAG, "Seeding another 'Text Entry' assignment for '${course.name}' course.")
+ val assignment2 = AssignmentsApi.createAssignment(course.id, teacher.token, withDescription = true, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), dueAt = 1.days.fromNow.iso8601)
+
+ Log.d(PREPARATION_TAG, "Seeding quiz for '${course.name}' course.")
+ val quiz = QuizzesApi.createQuiz(course.id, teacher.token, withDescription = true, dueAt = 3.days.fromNow.iso8601)
+
+ Log.d(PREPARATION_TAG, "Create an unpublished page for course: '${course.name}'.")
+ val testPage = PagesApi.createCoursePage(course.id, teacher.token, published = false, body = "")
+
+ Log.d(PREPARATION_TAG, "Create another unpublished page for course: '${course.name}'.")
+ val testPage2 = PagesApi.createCoursePage(course.id, teacher.token, published = true, frontPage = false, body = "")
+
+ Log.d(PREPARATION_TAG, "Create a discussion topic for '${course.name}' course.")
+ val discussionTopic = DiscussionTopicsApi.createDiscussion(courseId = course.id, token = teacher.token)
+
+ Log.d(PREPARATION_TAG, "Seeding a module for '${course.name}' course. It starts as unpublished.")
+ val module = ModulesApi.createModule(course.id, teacher.token)
+
+ Log.d(PREPARATION_TAG, "Seeding another module for '${course.name}' course. It starts as unpublished.")
+ val module2 = ModulesApi.createModule(course.id, teacher.token)
+
+ Log.d(PREPARATION_TAG,"Associate '${assignment.name}' assignment with module: '${module.id}'.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module.id, moduleItemTitle = assignment.name, moduleItemType = ModuleItemTypes.ASSIGNMENT.stringVal, contentId = assignment.id.toString())
+
+ Log.d(PREPARATION_TAG,"Associate '${quiz.title}' quiz with module: '${module.id}'.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module.id, moduleItemTitle = quiz.title, moduleItemType = ModuleItemTypes.QUIZ.stringVal, contentId = quiz.id.toString())
+
+ Log.d(PREPARATION_TAG,"Associate '${testPage.title}' page with module: '${module.id}'.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module.id, moduleItemTitle = testPage.title, moduleItemType = ModuleItemTypes.PAGE.stringVal, contentId = null, pageUrl = testPage.url)
+
+ Log.d(PREPARATION_TAG,"Associate '${discussionTopic.title}' discussion with module: '${module.id}'.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module.id, moduleItemTitle = discussionTopic.title, moduleItemType = ModuleItemTypes.DISCUSSION.stringVal, contentId = discussionTopic.id.toString())
+
+ Log.d(PREPARATION_TAG, "Associate '${assignment2.name}' assignment with module: '${module2.id}'.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module2.id , assignment2.name, ModuleItemTypes.ASSIGNMENT.stringVal, assignment2.id.toString())
+
+ Log.d(PREPARATION_TAG, "Associate '${testPage2.title}' page with module: '${module2.id}'.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module2.id, testPage2.title, ModuleItemTypes.PAGE.stringVal, null, pageUrl = testPage2.url)
+
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'. Assert that '${course.name}' course is displayed on the Dashboard.")
+ tokenLogin(teacher)
+ dashboardPage.waitForRender()
+
+ Log.d(STEP_TAG, "Open '${course.name}' course and navigate to Modules Page.")
+ dashboardPage.openCourse(course.name)
+ courseBrowserPage.openModulesTab()
+
+ Log.d(STEP_TAG, "Assert that '${module.name}' and '${module2.name}' modules are displayed and they are unpublished by default. Assert that the '${testPage.title}' page module item is not published and the other module items are published in '${module.name}' module.")
+ moduleListPage.assertModuleIsDisplayed(module.name)
+ moduleListPage.assertModuleNotPublished(module.name)
+ moduleListPage.assertModuleIsDisplayed(module2.name)
+ moduleListPage.assertModuleNotPublished(module2.name)
+ moduleListPage.assertModuleItemIsPublished(assignment.name)
+ moduleListPage.assertModuleItemIsPublished(quiz.title)
+ moduleListPage.assertModuleItemIsPublished(discussionTopic.title)
+ moduleListPage.assertModuleItemNotPublished(testPage.title)
+
+ //Upper layer - All Modules and Items
+ Log.d(STEP_TAG, "Open Module List Page overflow menu and assert that the corresponding menu items are displayed.")
+ openOverflowMenu()
+ moduleListPage.assertToolbarMenuItems()
+
+ Log.d(STEP_TAG, "Click on 'Publish all Modules and Items' and confirm it via the publish dialog.")
+ moduleListPage.clickOnText(R.string.publishAllModulesAndItems)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'All Modules and Items' is displayed as title and the corresponding note also displayed on the Progress Page. Click on 'Done' on the Progress Page once it finished.")
+ progressPage.assertProgressPageTitle(R.string.allModulesAndItems)
+ progressPage.assertProgressPageNote(R.string.moduleBulkUpdateNote)
+ progressPage.clickDone()
+
+ Log.d(STEP_TAG, "Assert that the proper snack bar text is displayed and the '${module.name}' module and all of it's items became published.")
+ moduleListPage.assertSnackbarText(R.string.allModulesAndAllItemsPublished)
+ moduleListPage.assertModuleIsPublished(module.name)
+ moduleListPage.assertModuleItemIsPublished(assignment.name)
+ moduleListPage.assertModuleItemIsPublished(quiz.title)
+ moduleListPage.assertModuleItemIsPublished(testPage.title)
+ moduleListPage.assertModuleItemIsPublished(discussionTopic.title)
+
+ Log.d(STEP_TAG, "Assert that '${module2.name}' module and all of it's items became published.")
+ moduleListPage.assertModuleIsPublished(module2.name)
+ moduleListPage.assertModuleItemIsPublished(assignment2.name)
+ moduleListPage.assertModuleItemIsPublished(testPage2.title)
+
+ Log.d(STEP_TAG, "Open Module List Page overflow menu")
+ openOverflowMenu()
+
+ Log.d(STEP_TAG, "Click on 'Unpublish all Modules and Items' and confirm it via the unpublish dialog.")
+ moduleListPage.clickOnText(R.string.unpublishAllModulesAndItems)
+ moduleListPage.clickOnText(R.string.unpublishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'All Modules and Items' is displayed as title on the Progress page. Click on 'Done' on the Progress Page once it finished.")
+ progressPage.assertProgressPageTitle(R.string.allModulesAndItems)
+ progressPage.clickDone()
+
+ Log.d(STEP_TAG, "Assert that the proper snack bar text is displayed and the '${module.name}' module and all of it's items became unpublished.")
+ moduleListPage.assertSnackbarText(R.string.allModulesAndAllItemsUnpublished)
+ moduleListPage.assertModuleNotPublished(module.name)
+ moduleListPage.assertModuleItemNotPublished(assignment.name)
+ moduleListPage.assertModuleItemNotPublished(quiz.title)
+ moduleListPage.assertModuleItemNotPublished(testPage.title)
+ moduleListPage.assertModuleItemNotPublished(discussionTopic.title)
+
+ Log.d(STEP_TAG, "Assert that '${module2.name}' module and all of it's items became unpublished.")
+ moduleListPage.assertModuleNotPublished(module2.name)
+ moduleListPage.assertModuleItemNotPublished(assignment2.name)
+ moduleListPage.assertModuleItemNotPublished(testPage2.title)
+
+ Log.d(STEP_TAG, "Open Module List Page overflow menu")
+ openOverflowMenu()
+
+ Log.d(STEP_TAG, "Click on 'Publish Modules only' and confirm it via the publish dialog.")
+ moduleListPage.clickOnText(R.string.publishModulesOnly)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'All Modules' title is displayed on the Progress page. Click on 'Done' on the Progress Page once it finished.")
+ progressPage.assertProgressPageTitle(R.string.allModules)
+ progressPage.clickDone()
+
+ Log.d(STEP_TAG, "Assert that the proper snack bar text is displayed and only the '${module.name}' module became published, but it's items remaining unpublished.")
+ moduleListPage.assertSnackbarText(R.string.onlyModulesPublished)
+ moduleListPage.assertModuleIsPublished(module.name)
+ moduleListPage.assertModuleItemNotPublished(assignment.name)
+ moduleListPage.assertModuleItemNotPublished(quiz.title)
+ moduleListPage.assertModuleItemNotPublished(testPage.title)
+ moduleListPage.assertModuleItemNotPublished(discussionTopic.title)
+
+ Log.d(STEP_TAG, "Assert that '${module2.name}' module became published but all of it's items are remaining unpublished.")
+ moduleListPage.assertModuleIsPublished(module2.name)
+ moduleListPage.assertModuleItemNotPublished(assignment2.name)
+ moduleListPage.assertModuleItemNotPublished(testPage2.title)
+
+ //Middle layer - One Module and Items
+
+ Log.d(STEP_TAG, "Click on '${module.name}' module overflow and assert that the corresponding menu items are displayed.")
+ moduleListPage.clickItemOverflow(module.name)
+ moduleListPage.assertModuleMenuItems()
+
+ Log.d(STEP_TAG, "Click on 'Publish Module and all Items' and confirm it via the publish dialog.")
+ moduleListPage.clickOnText(R.string.publishModuleAndItems)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'Selected Modules and Items' is displayed as title on the Progress page. Click on 'Done' on the Progress Page once it finished.")
+ progressPage.assertProgressPageTitle(R.string.selectedModulesAndItems)
+ progressPage.clickDone()
+
+ Log.d(STEP_TAG, "Assert that the proper snack bar text is displayed and the '${module.name}' module and all of it's items became published.")
+ moduleListPage.assertSnackbarText(R.string.moduleAndAllItemsPublished)
+ moduleListPage.assertModuleIsPublished(module.name)
+ moduleListPage.assertModuleItemIsPublished(assignment.name)
+ moduleListPage.assertModuleItemIsPublished(quiz.title)
+ moduleListPage.assertModuleItemIsPublished(testPage.title)
+ moduleListPage.assertModuleItemIsPublished(discussionTopic.title)
+
+ Log.d(STEP_TAG, "Click on '${module.name}' module overflow.")
+ moduleListPage.clickItemOverflow(module.name)
+
+ Log.d(STEP_TAG, "Click on 'Unpublish Module and all Items' and confirm it via the unpublish dialog.")
+ moduleListPage.clickOnText(R.string.unpublishModuleAndItems)
+ moduleListPage.clickOnText(R.string.unpublishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'Selected Modules and Items' is displayed as title on the Progress page. Click on 'Done' on the Progress Page once it finished.")
+ progressPage.assertProgressPageTitle(R.string.selectedModulesAndItems)
+ progressPage.clickDone()
+
+ Log.d(STEP_TAG, "Assert that the proper snack bar text is displayed and the '${module.name}' module and all of it's items became unpublished.")
+ moduleListPage.assertSnackbarText(R.string.moduleAndAllItemsUnpublished)
+ moduleListPage.assertModuleNotPublished(module.name)
+ moduleListPage.assertModuleItemNotPublished(assignment.name)
+ moduleListPage.assertModuleItemNotPublished(quiz.title)
+ moduleListPage.assertModuleItemNotPublished(testPage.title)
+ moduleListPage.assertModuleItemNotPublished(discussionTopic.title)
+
+ Log.d(STEP_TAG, "Click on '${module.name}' module overflow.")
+ moduleListPage.clickItemOverflow(module.name)
+
+ Log.d(STEP_TAG, "Click on 'Publish Module only' and confirm it via the publish dialog.")
+ moduleListPage.clickOnText(R.string.publishModuleOnly)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+ device.waitForWindowUpdate(null, 3000)
+ device.waitForIdle()
+
+ Log.d(STEP_TAG, "Assert that only the '${module.name}' module became published, but it's items remaining unpublished.")
+ moduleListPage.assertModuleIsPublished(module.name)
+ moduleListPage.assertModuleItemNotPublished(assignment.name)
+ moduleListPage.assertModuleItemNotPublished(quiz.title)
+ moduleListPage.assertModuleItemNotPublished(testPage.title)
+ moduleListPage.assertModuleItemNotPublished(discussionTopic.title)
+
+ //Bottom layer - One module item
+
+ Log.d(STEP_TAG, "Click on '${assignment.name}' assignment's overflow menu and publish it. Confirm the publish via the publish dialog.")
+ moduleListPage.clickItemOverflow(assignment.name)
+ moduleListPage.clickOnText(R.string.publishModuleItemAction)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'Item published' snack bar has displayed and the '${assignment.name}' assignment became published.")
+ moduleListPage.assertSnackbarText(R.string.moduleItemPublished)
+ moduleListPage.assertModuleItemIsPublished(assignment.name)
+
+ Log.d(STEP_TAG, "Click on '${quiz.title}' quiz's overflow menu and publish it. Confirm the publish via the publish dialog.")
+ moduleListPage.clickItemOverflow(quiz.title)
+ moduleListPage.clickOnText(R.string.publishModuleItemAction)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'Item published' snack bar has displayed and the '${quiz.title}' quiz became published.")
+ moduleListPage.assertSnackbarText(R.string.moduleItemPublished)
+ moduleListPage.assertModuleItemIsPublished(quiz.title)
+
+ Log.d(STEP_TAG, "Click on '${testPage.title}' page's overflow menu and publish it. Confirm the publish via the publish dialog.")
+ moduleListPage.clickItemOverflow(testPage.title)
+ moduleListPage.clickOnText(R.string.publishModuleItemAction)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'Item published' snack bar has displayed and the '${testPage.title}' page module item became published.")
+ moduleListPage.assertSnackbarText(R.string.moduleItemPublished)
+ moduleListPage.assertModuleItemIsPublished(assignment.name)
+
+ Log.d(STEP_TAG, "Click on '${discussionTopic.title}' discussion topic's overflow menu and publish it. Confirm the publish via the publish dialog.")
+ moduleListPage.clickItemOverflow(discussionTopic.title)
+ moduleListPage.clickOnText(R.string.publishModuleItemAction)
+ moduleListPage.clickOnText(R.string.publishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'Item published' snack bar has displayed and the '${discussionTopic.title}' discussion topic became published.")
+ moduleListPage.assertSnackbarText(R.string.moduleItemPublished)
+ moduleListPage.assertModuleItemIsPublished(discussionTopic.title)
+
+ Log.d(STEP_TAG, "Click on '${assignment.name}' assignment's overflow menu and unpublish it. Confirm the unpublish via the unpublish dialog.")
+ moduleListPage.clickItemOverflow(assignment.name)
+ moduleListPage.clickOnText(R.string.unpublishModuleItemAction)
+ moduleListPage.clickOnText(R.string.unpublishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'Item unpublished' snack bar has displayed and the '${assignment.name}' assignment became unpublished.")
+ moduleListPage.assertSnackbarText(R.string.moduleItemUnpublished)
+ moduleListPage.assertModuleItemNotPublished(assignment.name)
+
+ Log.d(STEP_TAG, "Click on '${quiz.title}' quiz's overflow menu and unpublish it. Confirm the unpublish via the unpublish dialog.")
+ moduleListPage.clickItemOverflow(quiz.title)
+ moduleListPage.clickOnText(R.string.unpublishModuleItemAction)
+ moduleListPage.clickOnText(R.string.unpublishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'Item unpublished' snack bar has displayed and the '${quiz.title}' quiz became unpublished.")
+ moduleListPage.assertSnackbarText(R.string.moduleItemUnpublished)
+ moduleListPage.assertModuleItemNotPublished(quiz.title)
+
+ Log.d(STEP_TAG, "Click on '${testPage.title}' page overflow menu and unpublish it. Confirm the unpublish via the unpublish dialog.")
+ moduleListPage.clickItemOverflow(testPage.title)
+ moduleListPage.clickOnText(R.string.unpublishModuleItemAction)
+ moduleListPage.clickOnText(R.string.unpublishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'Item unpublished' snack bar has displayed and the '${testPage.title}' page module item became unpublished.")
+ moduleListPage.assertSnackbarText(R.string.moduleItemUnpublished)
+ moduleListPage.assertModuleItemNotPublished(testPage.title)
+
+ Log.d(STEP_TAG, "Click on '${discussionTopic.title}' discussion topic's overflow menu and unpublish it. Confirm the unpublish via the unpublish dialog.")
+ moduleListPage.clickItemOverflow(discussionTopic.title)
+ moduleListPage.clickOnText(R.string.unpublishModuleItemAction)
+ moduleListPage.clickOnText(R.string.unpublishDialogPositiveButton)
+
+ Log.d(STEP_TAG, "Assert that the 'Item unpublished' snack bar has displayed and the '${discussionTopic.title}' discussion topic became unpublished.")
+ moduleListPage.assertSnackbarText(R.string.moduleItemUnpublished)
+ moduleListPage.assertModuleItemNotPublished(discussionTopic.title)
+ }
}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt
index eb42f20fbd..4535f058d9 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt
@@ -9,9 +9,6 @@ import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.dataseeding.api.PagesApi
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
-import com.instructure.dataseeding.model.PageApiModel
import com.instructure.teacher.ui.pages.WebViewTextCheck
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.seedData
@@ -21,6 +18,7 @@ import org.junit.Test
@HiltAndroidTest
class PagesE2ETest : TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -35,87 +33,87 @@ class PagesE2ETest : TeacherTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Create an unpublished page for course: ${course.name}.")
- val testPage1 = createCoursePage(course, teacher, published = false, frontPage = false, body = "")
+ Log.d(PREPARATION_TAG,"Create an unpublished page for course: '${course.name}'.")
+ val unpublishedPage = PagesApi.createCoursePage(course.id, teacher.token, published = false, frontPage = false, body = "")
- Log.d(PREPARATION_TAG,"Create a published page for course: ${course.name}.")
- val testPage2 = createCoursePage(course, teacher, published = true, frontPage = false, body = "")
+ Log.d(PREPARATION_TAG,"Create a published page for course: '${course.name}'.")
+ val publishedPage = PagesApi.createCoursePage(course.id, teacher.token, published = true, frontPage = false, body = "")
- Log.d(PREPARATION_TAG,"Create a front page for course: ${course.name}.")
- val testPage3 = createCoursePage(course, teacher, published = true, frontPage = true, body = "")
+ Log.d(PREPARATION_TAG,"Create a front page for course: '${course.name}'.")
+ val frontPage = PagesApi.createCoursePage(course.id, teacher.token, published = true, frontPage = true, body = "")
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
- Log.d(STEP_TAG,"Open ${course.name} course and navigate to Pages Page.")
+ Log.d(STEP_TAG,"Open '${course.name}' course and navigate to Pages Page.")
dashboardPage.openCourse(course.name)
courseBrowserPage.openPagesTab()
- Log.d(STEP_TAG,"Assert that ${testPage1.title} page is displayed and it is really unpublished.")
- pageListPage.assertPageDisplayed(testPage1.title)
- pageListPage.assertPageIsUnpublished(testPage1.title)
+ Log.d(STEP_TAG,"Assert that '${unpublishedPage.title}' page is displayed and it is really unpublished.")
+ pageListPage.assertPageDisplayed(unpublishedPage.title)
+ pageListPage.assertPageIsUnpublished(unpublishedPage.title)
- Log.d(STEP_TAG,"Assert that ${testPage2.title} page is displayed and it is really published.")
- pageListPage.assertPageDisplayed(testPage2.title)
- pageListPage.assertPageIsPublished(testPage2.title)
+ Log.d(STEP_TAG,"Assert that '${publishedPage.title}' page is displayed and it is really published.")
+ pageListPage.assertPageDisplayed(publishedPage.title)
+ pageListPage.assertPageIsPublished(publishedPage.title)
- Log.d(STEP_TAG,"Assert that ${testPage3.title} page is displayed and it is really a front page and published.")
- pageListPage.assertPageDisplayed(testPage3.title)
- pageListPage.assertPageIsPublished(testPage3.title)
- pageListPage.assertFrontPageDisplayed(testPage3.title)
+ Log.d(STEP_TAG,"Assert that '${frontPage.title}' page is displayed and it is really a front page and published.")
+ pageListPage.assertPageDisplayed(frontPage.title)
+ pageListPage.assertPageIsPublished(frontPage.title)
+ pageListPage.assertFrontPageDisplayed(frontPage.title)
- Log.d(STEP_TAG,"Open ${testPage2.title} page. Assert that it is really a regular published page via web view assertions.")
- pageListPage.openPage(testPage2.title)
+ Log.d(STEP_TAG,"Open '${publishedPage.title}' page. Assert that it is really a regular published page via web view assertions.")
+ pageListPage.openPage(publishedPage.title)
editPageDetailsPage.runTextChecks(WebViewTextCheck(Locator.ID, "header1", "Regular Page Text"))
Log.d(STEP_TAG,"Navigate back to Pages page.")
Espresso.pressBack()
- Log.d(STEP_TAG,"Open ${testPage3.title} page. Assert that it is really a front (published) page via web view assertions.")
- pageListPage.openPage(testPage3.title)
+ Log.d(STEP_TAG,"Open '${frontPage.title}' page. Assert that it is really a front (published) page via web view assertions.")
+ pageListPage.openPage(frontPage.title)
editPageDetailsPage.runTextChecks(WebViewTextCheck(Locator.ID, "header1", "Front Page Text"))
Log.d(STEP_TAG,"Navigate back to Pages page.")
Espresso.pressBack()
- Log.d(STEP_TAG,"Open ${testPage1.title} page. Assert that it is really an unpublished page via web view assertions.")
- pageListPage.openPage(testPage1.title)
+ Log.d(STEP_TAG,"Open '${unpublishedPage.title}' page. Assert that it is really an unpublished page via web view assertions.")
+ pageListPage.openPage(unpublishedPage.title)
editPageDetailsPage.runTextChecks(WebViewTextCheck(Locator.ID, "header1", "Unpublished Page Text"))
Espresso.pressBack()
val editedUnpublishedPageName = "Page still unpublished"
- Log.d(STEP_TAG,"Open and edit the ${testPage1.title} page and set $editedUnpublishedPageName page name as new value. Click on 'Save' and navigate back.")
- pageListPage.openPage(testPage1.title)
+ Log.d(STEP_TAG,"Open and edit the '${unpublishedPage.title}' page and set '$editedUnpublishedPageName' page name as new value. Click on 'Save' and navigate back.")
+ pageListPage.openPage(unpublishedPage.title)
editPageDetailsPage.openEdit()
editPageDetailsPage.editPageName(editedUnpublishedPageName)
editPageDetailsPage.savePage()
Espresso.pressBack()
- Log.d(STEP_TAG,"Assert that the page name has been changed to $editedUnpublishedPageName.")
+ Log.d(STEP_TAG,"Assert that the page name has been changed to '$editedUnpublishedPageName'.")
pageListPage.assertPageIsUnpublished(editedUnpublishedPageName)
- Log.d(STEP_TAG,"Open ${testPage2.title} page and Edit it. Set it as a front page and click on 'Save'. Navigate back.")
- pageListPage.openPage(testPage2.title)
+ Log.d(STEP_TAG,"Open '${publishedPage.title}' page and Edit it. Set it as a front page and click on 'Save'. Navigate back.")
+ pageListPage.openPage(publishedPage.title)
editPageDetailsPage.openEdit()
editPageDetailsPage.toggleFrontPage()
editPageDetailsPage.savePage()
Espresso.pressBack()
- Log.d(STEP_TAG,"Assert that ${testPage2.title} is displayed as a front page.")
- pageListPage.assertFrontPageDisplayed(testPage2.title)
+ Log.d(STEP_TAG,"Assert that '${publishedPage.title}' is displayed as a front page.")
+ pageListPage.assertFrontPageDisplayed(publishedPage.title)
- Log.d(STEP_TAG,"Open $editedUnpublishedPageName page and Edit it. Set it as a front page and click on 'Save'. Navigate back.")
+ Log.d(STEP_TAG,"Open '$editedUnpublishedPageName' page and Edit it. Set it as a front page and click on 'Save'. Navigate back.")
pageListPage.openPage(editedUnpublishedPageName)
editPageDetailsPage.openEdit()
editPageDetailsPage.togglePublished()
editPageDetailsPage.savePage()
Espresso.pressBack()
- Log.d(STEP_TAG,"Assert that $testPage2 is published.")
- pageListPage.assertPageIsPublished(testPage2.title)
+ Log.d(STEP_TAG,"Assert that '$publishedPage' is published.")
+ pageListPage.assertPageIsPublished(publishedPage.title)
- Log.d(STEP_TAG,"Open ${testPage3.title} page and Edit it. Unpublish it and remove 'Front page' from it.")
- pageListPage.openPage(testPage3.title)
+ Log.d(STEP_TAG,"Open '${frontPage.title}' page and Edit it. Unpublish it and remove 'Front page' from it.")
+ pageListPage.openPage(frontPage.title)
editPageDetailsPage.openEdit()
editPageDetailsPage.togglePublished()
editPageDetailsPage.toggleFrontPage()
@@ -123,13 +121,13 @@ class PagesE2ETest : TeacherTest() {
Log.d(STEP_TAG,"Assert that a front page cannot be unpublished.")
editPageDetailsPage.unableToSaveUnpublishedFrontPage()
- Log.d(STEP_TAG,"Publish ${testPage3.title} page again. Click on 'Save' and navigate back-")
+ Log.d(STEP_TAG,"Publish '${frontPage.title}' page again. Click on 'Save' and navigate back-")
editPageDetailsPage.togglePublished()
editPageDetailsPage.savePage()
Espresso.pressBack()
- Log.d(STEP_TAG,"Assert that ${testPage2.title} is displayed as a front page.")
- pageListPage.assertFrontPageDisplayed(testPage2.title)
+ Log.d(STEP_TAG,"Assert that '${publishedPage.title}' is displayed as a front page.")
+ pageListPage.assertFrontPageDisplayed(publishedPage.title)
Log.d(STEP_TAG,"Click on '+' icon on the UI to create a new page.")
pageListPage.clickOnCreateNewPage()
@@ -159,20 +157,4 @@ class PagesE2ETest : TeacherTest() {
pageListPage.assertPageCount(4)
}
- private fun createCoursePage(
- course: CourseApiModel,
- teacher: CanvasUserApiModel,
- published: Boolean = true,
- frontPage: Boolean = false,
- body: String = EMPTY_STRING
- ): PageApiModel {
- return PagesApi.createCoursePage(
- courseId = course.id,
- published = published,
- frontPage = frontPage,
- token = teacher.token,
- body = body
- )
- }
-
}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt
index 98272c9829..f5b64c3b61 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt
@@ -25,9 +25,6 @@ import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.dataseeding.api.GroupsApi
import com.instructure.dataseeding.api.SubmissionsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
@@ -46,6 +43,7 @@ import java.lang.Thread.sleep
@HiltAndroidTest
class PeopleE2ETest: TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -69,13 +67,13 @@ class PeopleE2ETest: TeacherTest() {
val group = GroupsApi.createGroup(groupCategory.id, teacher.token)
val group2 = GroupsApi.createGroup(groupCategory2.id, teacher.token)
- Log.d(PREPARATION_TAG,"Create group membership for ${gradedStudent.name} student to '${group.name}' group.")
+ Log.d(PREPARATION_TAG,"Create group membership for '${gradedStudent.name}' student to '${group.name}' group.")
GroupsApi.createGroupMembership(group.id, gradedStudent.id, teacher.token)
- Log.d(PREPARATION_TAG,"Create group membership for ${notGradedStudent.name} student to '${group2.name}' group.")
+ Log.d(PREPARATION_TAG,"Create group membership for '${notGradedStudent.name}' student to '${group2.name}' group.")
GroupsApi.createGroupMembership(group2.id, notGradedStudent.id, teacher.token)
- Log.d(PREPARATION_TAG,"Seed a 'Text Entry' assignment for course: ${course.name}.")
+ Log.d(PREPARATION_TAG,"Seed a 'Text Entry' assignment for course: '${course.name}'.")
val assignments = seedAssignments(
courseId = course.id,
dueAt = 1.days.fromNow.iso8601,
@@ -84,7 +82,7 @@ class PeopleE2ETest: TeacherTest() {
pointsPossible = 10.0
)
- Log.d(PREPARATION_TAG,"Seed a submission for ${assignments[0].name} assignment.")
+ Log.d(PREPARATION_TAG,"Seed a submission for '${assignments[0].name}' assignment.")
seedAssignmentSubmission(
submissionSeeds = listOf(SubmissionsApi.SubmissionSeedInfo(
amount = 1,
@@ -95,13 +93,13 @@ class PeopleE2ETest: TeacherTest() {
studentToken = gradedStudent.token
)
- Log.d(PREPARATION_TAG,"Grade the previously seeded submission for ${assignments[0].name} assignment.")
- gradeSubmission(teacher, course, assignments, gradedStudent)
+ Log.d(PREPARATION_TAG,"Grade the previously seeded submission for '${assignments[0].name}' assignment.")
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, assignments[0].id, gradedStudent.id, postedGrade = "10")
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
- Log.d(STEP_TAG,"Open ${course.name} course and navigate to People Page.")
+ Log.d(STEP_TAG,"Open '${course.name}' course and navigate to People Page.")
dashboardPage.openCourse(course.name)
courseBrowserPage.openPeopleTab()
@@ -118,7 +116,7 @@ class PeopleE2ETest: TeacherTest() {
personContextPage.assertDisplaysCourseInfo(course)
personContextPage.assertSectionNameView(PersonContextPage.UserRole.OBSERVER)
- Log.d(STEP_TAG,"Navigate back and click on ${notGradedStudent.name} student and assert that the NOT GRADED student course info and the corresponding section name is displayed are displayed properly on Context Page.")
+ Log.d(STEP_TAG,"Navigate back and click on '${notGradedStudent.name}' student and assert that the NOT GRADED student course info and the corresponding section name is displayed are displayed properly on Context Page.")
Espresso.pressBack()
peopleListPage.assertPersonRole(notGradedStudent.name, PeopleListPage.UserRole.STUDENT)
peopleListPage.clickPerson(notGradedStudent)
@@ -128,7 +126,7 @@ class PeopleE2ETest: TeacherTest() {
studentContextPage.assertStudentGrade("--")
studentContextPage.assertStudentSubmission("--")
- Log.d(STEP_TAG,"Navigate back and click on ${gradedStudent.name} student." +
+ Log.d(STEP_TAG,"Navigate back and click on '${gradedStudent.name}' student." +
"Assert that '${gradedStudent.name}' graded student's info," +
"and the '${course.name}' course's info are displayed properly on the Context Page.")
Espresso.pressBack()
@@ -145,7 +143,7 @@ class PeopleE2ETest: TeacherTest() {
studentContextPage.clickOnNewMessageButton()
val subject = "Test Subject"
- Log.d(STEP_TAG,"Fill in the 'Subject' field with the value: $subject. Add some message text and click on 'Send' (aka. 'Arrow') button.")
+ Log.d(STEP_TAG,"Fill in the 'Subject' field with the value: '$subject'. Add some message text and click on 'Send' (aka. 'Arrow') button.")
addMessagePage.composeMessageWithSubject(subject, "This a test message from student context page.")
addMessagePage.clickSendButton()
@@ -216,19 +214,4 @@ class PeopleE2ETest: TeacherTest() {
inboxPage.assertHasConversation()
}
- private fun gradeSubmission(
- teacher: CanvasUserApiModel,
- course: CourseApiModel,
- assignments: List,
- gradedStudent: CanvasUserApiModel
- ) {
- SubmissionsApi.gradeSubmission(
- teacherToken = teacher.token,
- courseId = course.id,
- assignmentId = assignments[0].id,
- studentId = gradedStudent.id,
- postedGrade = "10",
- excused = false
- )
- }
}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt
index 66298f5d5f..2864daabc4 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt
@@ -36,6 +36,7 @@ import org.junit.Test
@HiltAndroidTest
class QuizE2ETest: TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -51,63 +52,48 @@ class QuizE2ETest: TeacherTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
- Log.d(STEP_TAG,"Open ${course.name} course and navigate to Quizzes Page.")
+ Log.d(STEP_TAG,"Open '${course.name}' course and navigate to Quizzes Page.")
dashboardPage.openCourse(course.name)
courseBrowserPage.openQuizzesTab()
Log.d(STEP_TAG,"Assert that there is no quiz displayed on the page.")
quizListPage.assertDisplaysNoQuizzesView()
- Log.d(PREPARATION_TAG,"Seed a quiz for the ${course.name} course. Also, seed a question into the quiz and publish it.")
- val testQuizList = seedQuizzes(
- courseId = course.id,
- withDescription = true,
- dueAt = 3.days.fromNow.iso8601,
- teacherToken = teacher.token,
- published = false
- )
-
- seedQuizQuestion(
- courseId = course.id,
- quizId = testQuizList.quizList[0].id,
- teacherToken = teacher.token
- )
-
- Log.d(STEP_TAG,"Refresh the page. Assert that the quiz is there and click on the previously seeded quiz: ${testQuizList.quizList[0].title}.")
+ Log.d(PREPARATION_TAG,"Seed a quiz for the '${course.name}' course. Also, seed a question into the quiz and publish it.")
+ val testQuizList = seedQuizzes(courseId = course.id, withDescription = true, dueAt = 3.days.fromNow.iso8601, teacherToken = teacher.token, published = false)
+ seedQuizQuestion(courseId = course.id, quizId = testQuizList.quizList[0].id, teacherToken = teacher.token)
+
+ Log.d(STEP_TAG,"Refresh the page. Assert that the quiz is there and click on the previously seeded quiz: '${testQuizList.quizList[0].title}'.")
quizListPage.refresh()
quizListPage.clickQuiz(testQuizList.quizList[0].title)
- Log.d(STEP_TAG,"Assert that ${testQuizList.quizList[0].title} quiz is 'Not Submitted' and it is unpublished.")
+ Log.d(STEP_TAG,"Assert that '${testQuizList.quizList[0].title}' quiz is 'Not Submitted' and it is unpublished.")
quizDetailsPage.assertNotSubmitted()
quizDetailsPage.assertQuizUnpublished()
val newQuizTitle = "This is a new quiz"
- Log.d(STEP_TAG,"Open 'Edit' page and edit the ${testQuizList.quizList[0].title} quiz's title to: $newQuizTitle.")
+ Log.d(STEP_TAG,"Open 'Edit' page and edit the '${testQuizList.quizList[0].title}' quiz's title to: '$newQuizTitle'.")
quizDetailsPage.openEditPage()
editQuizDetailsPage.editQuizTitle(newQuizTitle)
- Log.d(STEP_TAG,"Assert that the quiz name has been changed to: $newQuizTitle.")
+ Log.d(STEP_TAG,"Assert that the quiz name has been changed to: '$newQuizTitle'.")
quizDetailsPage.assertQuizNameChanged(newQuizTitle)
- Log.d(STEP_TAG,"Open 'Edit' page and switch on the 'Published' checkbox, so publish the $newQuizTitle quiz. Click on 'Save'.")
+ Log.d(STEP_TAG,"Open 'Edit' page and switch on the 'Published' checkbox, so publish the '$newQuizTitle' quiz. Click on 'Save'.")
quizDetailsPage.openEditPage()
editQuizDetailsPage.switchPublish()
editQuizDetailsPage.saveQuiz()
- Log.d(STEP_TAG,"Refresh the page. Assert that $newQuizTitle quiz has been unpublished.")
+ Log.d(STEP_TAG,"Refresh the page. Assert that '$newQuizTitle' quiz has been unpublished.")
quizDetailsPage.refresh()
quizDetailsPage.assertQuizPublished()
- Log.d(PREPARATION_TAG,"Submit the ${testQuizList.quizList[0].title} quiz.")
- seedQuizSubmission(
- courseId = course.id,
- quizId = testQuizList.quizList[0].id,
- studentToken = student.token
- )
+ Log.d(PREPARATION_TAG,"Submit the '${testQuizList.quizList[0].title}' quiz.")
+ seedQuizSubmission(courseId = course.id, quizId = testQuizList.quizList[0].id, studentToken = student.token)
Log.d(STEP_TAG,"Refresh the page. Assert that it needs grading because of the previous submission.")
quizListPage.refresh()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt
index d7eeea79ac..dad19d8bac 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt
@@ -48,7 +48,7 @@ class SettingsE2ETest : TeacherTest() {
val data = seedData(students = 1, teachers = 1, courses = 1)
val teacher = data.teachersList[0]
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
@@ -64,11 +64,11 @@ class SettingsE2ETest : TeacherTest() {
profileSettingsPage.clickEditPencilIcon()
val newUserName = "John Doe"
- Log.d(STEP_TAG, "Edit username to: $newUserName. Click on 'Save' button.")
+ Log.d(STEP_TAG, "Edit username to: '$newUserName'. Click on 'Save' button.")
editProfileSettingsPage.editUserName(newUserName)
editProfileSettingsPage.clickOnSave()
- Log.d(STEP_TAG, "Assert that the username has been changed to $newUserName on the Profile Settings Page.")
+ Log.d(STEP_TAG, "Assert that the username has been changed to '$newUserName' on the Profile Settings Page.")
try {
Log.d(STEP_TAG, "Check if the user has landed on Settings Page. If yes, navigate back to Profile Settings Page.")
//Sometimes in Bitrise it's working different than locally, because in Bitrise sometimes the user has been navigated to Settings Page after saving a new name,
@@ -94,13 +94,13 @@ class SettingsE2ETest : TeacherTest() {
Log.d(STEP_TAG, "Press back button (without saving). The goal is to navigate back to the Profile Settings Page.")
Espresso.pressBack()
- Log.d(STEP_TAG, "Assert that the username value remained $newUserName.")
+ Log.d(STEP_TAG, "Assert that the username value remained '$newUserName'.")
profileSettingsPage.assertUserNameIs(newUserName)
} catch (e: NoMatchingViewException) {
Log.d(STEP_TAG, "Press back button (without saving). The goal is to navigate back to the Profile Settings Page.")
Espresso.pressBack()
- Log.d(STEP_TAG, "Assert that the username value remained $newUserName.")
+ Log.d(STEP_TAG, "Assert that the username value remained '$newUserName'.")
profileSettingsPage.assertUserNameIs(newUserName)
}
}
@@ -109,12 +109,13 @@ class SettingsE2ETest : TeacherTest() {
@Test
@TestMetaData(Priority.IMPORTANT, FeatureCategory.SETTINGS, TestCategory.E2E)
fun testDarkModeE2E() {
+
Log.d(PREPARATION_TAG, "Seeding data.")
val data = seedData(students = 1, teachers = 1, courses = 1)
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
@@ -134,7 +135,7 @@ class SettingsE2ETest : TeacherTest() {
Espresso.pressBack()
dashboardPage.assertCourseLabelTextColor("#FFFFFFFF")
- Log.d(STEP_TAG,"Select ${course.name} course and assert on the Course Browser Page that the tabs has the proper text color (which is used in Dark mode).")
+ Log.d(STEP_TAG,"Select '${course.name}' course and assert on the Course Browser Page that the tabs has the proper text color (which is used in Dark mode).")
dashboardPage.openCourse(course.name)
courseBrowserPage.assertTabLabelTextColor("Announcements","#FFFFFFFF")
courseBrowserPage.assertTabLabelTextColor("Assignments","#FFFFFFFF")
@@ -163,7 +164,7 @@ class SettingsE2ETest : TeacherTest() {
val data = seedData(students = 1, teachers = 1, courses = 1)
val teacher = data.teachersList[0]
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
@@ -185,7 +186,7 @@ class SettingsE2ETest : TeacherTest() {
val data = seedData(students = 1, teachers = 1, courses = 1)
val teacher = data.teachersList[0]
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
@@ -197,13 +198,13 @@ class SettingsE2ETest : TeacherTest() {
settingsPage.openAboutPage()
aboutPage.assertPageObjects()
- Log.d(STEP_TAG,"Check that domain is equal to: ${teacher.domain} (teacher's domain).")
+ Log.d(STEP_TAG,"Check that domain is equal to: '${teacher.domain}' (teacher's domain).")
aboutPage.domainIs(teacher.domain)
- Log.d(STEP_TAG,"Check that Login ID is equal to: ${teacher.loginId} (teacher's Login ID).")
+ Log.d(STEP_TAG,"Check that Login ID is equal to: '${teacher.loginId}' (teacher's Login ID).")
aboutPage.loginIdIs(teacher.loginId)
- Log.d(STEP_TAG,"Check that e-mail is equal to: ${teacher.loginId} (teacher's Login ID).")
+ Log.d(STEP_TAG,"Check that e-mail is equal to: '${teacher.loginId}' (teacher's Login ID).")
aboutPage.emailIs(teacher.loginId)
Log.d(STEP_TAG,"Assert that the Instructure company logo has been displayed on the About page.")
@@ -219,7 +220,7 @@ class SettingsE2ETest : TeacherTest() {
val data = seedData(students = 1, teachers = 1, courses = 1)
val teacher = data.teachersList[0]
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
@@ -244,7 +245,7 @@ class SettingsE2ETest : TeacherTest() {
val data = seedData(students = 1, teachers = 1, courses = 1)
val teacher = data.teachersList[0]
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
@@ -253,7 +254,7 @@ class SettingsE2ETest : TeacherTest() {
Log.d(PREPARATION_TAG,"Capture the initial remote config values.")
val initialValues = mutableMapOf()
- RemoteConfigParam.values().forEach {param -> initialValues.put(param.rc_name, RemoteConfigUtils.getString(param))}
+ RemoteConfigParam.values().forEach { param -> initialValues[param.rc_name] = RemoteConfigUtils.getString(param) }
Log.d(STEP_TAG,"Navigate to Remote Config Params Page.")
settingsPage.openRemoteConfigParamsPage()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt
index acf857613d..8231b11cb8 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt
@@ -30,9 +30,6 @@ import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvas.espresso.refresh
import com.instructure.canvas.espresso.withCustomConstraints
import com.instructure.dataseeding.api.SubmissionsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
@@ -49,6 +46,7 @@ import org.junit.Test
@HiltAndroidTest
class SpeedGraderE2ETest : TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -66,7 +64,7 @@ class SpeedGraderE2ETest : TeacherTest() {
val gradedStudent = data.studentsList[1]
val noSubStudent = data.studentsList[2]
- Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.")
+ Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.")
val assignment = seedAssignments(
courseId = course.id,
dueAt = 1.days.fromNow.iso8601,
@@ -75,7 +73,7 @@ class SpeedGraderE2ETest : TeacherTest() {
pointsPossible = 15.0
)
- Log.d(PREPARATION_TAG,"Seed a submission for ${assignment[0].name} assignment with ${student.name} student.")
+ Log.d(PREPARATION_TAG,"Seed a submission for '${assignment[0].name}' assignment with '${student.name}' student.")
seedAssignmentSubmission(
submissionSeeds = listOf(SubmissionsApi.SubmissionSeedInfo(
amount = 1,
@@ -86,7 +84,7 @@ class SpeedGraderE2ETest : TeacherTest() {
studentToken = student.token
)
- Log.d(PREPARATION_TAG,"Seed a submission for ${assignment[0].name} assignment with ${gradedStudent.name} student.")
+ Log.d(PREPARATION_TAG,"Seed a submission for '${assignment[0].name}' assignment with '${gradedStudent.name}' student.")
seedAssignmentSubmission(
submissionSeeds = listOf(SubmissionsApi.SubmissionSeedInfo(
amount = 1,
@@ -97,32 +95,32 @@ class SpeedGraderE2ETest : TeacherTest() {
studentToken = gradedStudent.token
)
- Log.d(PREPARATION_TAG,"Grade the previously seeded submission for ${gradedStudent.name} student.")
- gradeSubmission(teacher, course, assignment, gradedStudent)
+ Log.d(PREPARATION_TAG,"Grade the previously seeded submission for '${gradedStudent.name}' student.")
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, assignment[0].id, gradedStudent.id, postedGrade = "15")
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
- Log.d(STEP_TAG,"Open ${course.name} course and navigate to Assignments Page.")
+ Log.d(STEP_TAG,"Open '${course.name}' course and navigate to Assignments Page.")
dashboardPage.openCourse(course)
courseBrowserPage.openAssignmentsTab()
- Log.d(STEP_TAG,"Click on ${assignment[0].name} assignment and assert that that there is one 'Needs Grading' submission (for ${noSubStudent.name} student) and one 'Not Submitted' submission (for ${student.name} student. ")
+ Log.d(STEP_TAG,"Click on '${assignment[0].name}' assignment and assert that that there is one 'Needs Grading' submission for '${noSubStudent.name}' student and one 'Not Submitted' submission for '${student.name}' student.")
assignmentListPage.clickAssignment(assignment[0])
assignmentDetailsPage.assertNeedsGrading(actual = 1, outOf = 3)
assignmentDetailsPage.assertNotSubmitted(actual = 1, outOf = 3)
- Log.d(STEP_TAG,"Open 'Not Submitted' submissions and assert that the submission of ${noSubStudent.name} student is displayed. Navigate back.")
+ Log.d(STEP_TAG,"Open 'Not Submitted' submissions and assert that the submission of '${noSubStudent.name}' student is displayed. Navigate back.")
assignmentDetailsPage.openNotSubmittedSubmissions()
assignmentSubmissionListPage.assertHasStudentSubmission(noSubStudent)
Espresso.pressBack()
- Log.d(STEP_TAG,"Open 'Graded' submissions and assert that the submission of ${gradedStudent.name} student is displayed. Navigate back.")
+ Log.d(STEP_TAG,"Open 'Graded' submissions and assert that the submission of '${gradedStudent.name}' student is displayed. Navigate back.")
assignmentDetailsPage.openGradedSubmissions()
assignmentSubmissionListPage.assertHasStudentSubmission(gradedStudent)
Espresso.pressBack()
- Log.d(STEP_TAG,"Open (all) submissions and assert that the submission of ${student.name} student is displayed.")
+ Log.d(STEP_TAG,"Open (all) submissions and assert that the submission of '${student.name}' student is displayed.")
assignmentDetailsPage.openSubmissionsPage()
assignmentSubmissionListPage.clickSubmission(student)
speedGraderPage.assertDisplaysTextSubmissionViewWithStudentName(student.name)
@@ -132,7 +130,7 @@ class SpeedGraderE2ETest : TeacherTest() {
speedGraderGradePage.openGradeDialog()
val grade = "10"
- Log.d(STEP_TAG,"Enter $grade as the new grade and assert that it has applied. Navigate back and refresh the page.")
+ Log.d(STEP_TAG,"Enter '$grade' as the new grade and assert that it has applied. Navigate back and refresh the page.")
speedGraderGradePage.enterNewGrade(grade)
speedGraderGradePage.assertHasGrade(grade)
Espresso.pressBack()
@@ -165,7 +163,7 @@ class SpeedGraderE2ETest : TeacherTest() {
Log.d(STEP_TAG, "Navigate back assignment's details page.")
Espresso.pressBack()
- Log.d(STEP_TAG,"Open (all) submissions and assert that the submission of ${student.name} student is displayed.")
+ Log.d(STEP_TAG,"Open (all) submissions and assert that the submission of '${student.name}' student is displayed.")
assignmentDetailsPage.openSubmissionsPage()
Log.d(STEP_TAG, "Click on 'Post Policies' (eye) icon.")
@@ -191,19 +189,4 @@ class SpeedGraderE2ETest : TeacherTest() {
assignmentSubmissionListPage.assertGradesHidden(student.name)
}
- private fun gradeSubmission(
- teacher: CanvasUserApiModel,
- course: CourseApiModel,
- assignment: List,
- gradedStudent: CanvasUserApiModel
- ) {
- SubmissionsApi.gradeSubmission(
- teacherToken = teacher.token,
- courseId = course.id,
- assignmentId = assignment[0].id,
- studentId = gradedStudent.id,
- postedGrade = "15",
- excused = false
- )
- }
}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SyllabusE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SyllabusE2ETest.kt
index e64467cbb9..c5e7d3772e 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SyllabusE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SyllabusE2ETest.kt
@@ -2,20 +2,25 @@ package com.instructure.teacher.ui.e2e
import android.util.Log
import com.instructure.canvas.espresso.E2E
-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.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
-import com.instructure.teacher.ui.utils.*
+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.teacher.ui.utils.TeacherTest
+import com.instructure.teacher.ui.utils.seedAssignments
+import com.instructure.teacher.ui.utils.seedData
+import com.instructure.teacher.ui.utils.seedQuizzes
+import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
@HiltAndroidTest
class SyllabusE2ETest : TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -30,54 +35,41 @@ class SyllabusE2ETest : TeacherTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
- Log.d(STEP_TAG,"Open ${course.name} course and navigate to Syllabus Page.")
+ Log.d(STEP_TAG,"Open '${course.name}' course and navigate to Syllabus Page.")
dashboardPage.openCourse(course.name)
courseBrowserPage.openSyllabus()
Log.d(STEP_TAG,"Assert that empty view is displayed.")
syllabusPage.assertEmptyView()
- Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.")
- val assignment = seedAssignments(
- courseId = course.id,
- dueAt = 1.days.fromNow.iso8601,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- teacherToken = teacher.token,
- pointsPossible = 15.0,
- withDescription = true
- )
-
- Log.d(PREPARATION_TAG,"Seed a quiz for the ${course.name} course.")
- val quiz = seedQuizzes(
- courseId = course.id,
- withDescription = true,
- published = true,
- teacherToken = teacher.token,
- dueAt = 1.days.fromNow.iso8601
- )
-
- Log.d(STEP_TAG,"Refresh the Syllabus page and assert that the ${assignment[0].name} assignment and ${quiz.quizList[0].title} quiz are displayed as syllabus items.")
+ Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for '${course.name}' course.")
+ val assignment = seedAssignments(courseId = course.id, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), teacherToken = teacher.token, pointsPossible = 15.0, withDescription = true)
+
+ Log.d(PREPARATION_TAG,"Seed a quiz for the '${course.name}' course.")
+ val quiz = seedQuizzes(courseId = course.id, withDescription = true, published = true, teacherToken = teacher.token, dueAt = 1.days.fromNow.iso8601)
+
+ Log.d(STEP_TAG,"Refresh the Syllabus page and assert that the '${assignment[0].name}' assignment and '${quiz.quizList[0].title}' quiz are displayed as syllabus items.")
syllabusPage.refresh()
syllabusPage.assertItemDisplayed(assignment[0].name)
syllabusPage.assertItemDisplayed(quiz.quizList[0].title)
Log.d(STEP_TAG,"Refresh the Syllabus page. Click on 'Pencil' (aka. 'Edit') icon.")
-
syllabusPage.refresh()
syllabusPage.openEditSyllabus()
var syllabusBody = "Syllabus Body"
- Log.d(STEP_TAG,"Edit syllabus description (aka. 'Syllabus Body') by adding new value to it: $syllabusBody. Click on 'Save'.")
+
+ Log.d(STEP_TAG,"Edit syllabus description (aka. 'Syllabus Body') by adding new value to it: '$syllabusBody'. Click on 'Save'.")
editSyllabusPage.editSyllabusBody(syllabusBody)
editSyllabusPage.saveSyllabusEdit()
Log.d(STEP_TAG,"Assert that the previously made modifications has been applied on the syllabus.")
syllabusPage.assertDisplaysSyllabus(syllabusBody = syllabusBody, shouldDisplayTabs = true)
- Log.d(STEP_TAG,"Select 'Summary' Tab and assert that the ${assignment[0].name} assignment and ${quiz.quizList[0].title} quiz are displayed.")
+ Log.d(STEP_TAG,"Select 'Summary' Tab and assert that the '${assignment[0].name}' assignment and '${quiz.quizList[0].title}' quiz are displayed.")
syllabusPage.selectSummaryTab()
syllabusPage.assertItemDisplayed(assignment[0].name)
syllabusPage.assertItemDisplayed(quiz.quizList[0].title)
@@ -87,7 +79,7 @@ class SyllabusE2ETest : TeacherTest() {
syllabusBody = "Edited Syllabus Body"
syllabusPage.openEditSyllabus()
- Log.d(STEP_TAG,"Edit syllabus description (aka. 'Syllabus Body') by adding new value to it: $syllabusBody. Toggle 'Show course summary'. Click on 'Save'.")
+ Log.d(STEP_TAG,"Edit syllabus description (aka. 'Syllabus Body') by adding new value to it: '$syllabusBody'. Toggle 'Show course summary'. Click on 'Save'.")
editSyllabusPage.editSyllabusBody(syllabusBody)
editSyllabusPage.editSyllabusToggleShowSummary()
editSyllabusPage.saveSyllabusEdit()
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt
index bd6f0e0e61..40f3116be6 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt
@@ -18,18 +18,15 @@ package com.instructure.teacher.ui.e2e
import android.util.Log
import com.instructure.canvas.espresso.E2E
+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.dataseeding.api.SubmissionsApi
-import com.instructure.dataseeding.model.AssignmentApiModel
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.dataseeding.model.CourseApiModel
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.canvas.espresso.FeatureCategory
-import com.instructure.canvas.espresso.Priority
-import com.instructure.canvas.espresso.TestCategory
-import com.instructure.canvas.espresso.TestMetaData
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.seedAssignmentSubmission
import com.instructure.teacher.ui.utils.seedAssignments
@@ -40,6 +37,7 @@ import org.junit.Test
@HiltAndroidTest
class TodoE2ETest : TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -55,27 +53,17 @@ class TodoE2ETest : TeacherTest() {
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.")
- val assignments = seedAssignments(
- courseId = course.id,
- dueAt = 1.days.fromNow.iso8601,
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
- teacherToken = teacher.token,
- pointsPossible = 15.0
- )
+ Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.")
+ val assignments = seedAssignments(courseId = course.id, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), teacherToken = teacher.token, pointsPossible = 15.0)
- Log.d(PREPARATION_TAG,"Seed a submission for ${assignments[0].name} assignment with ${student.name} student.")
+ Log.d(PREPARATION_TAG,"Seed a submission for '${assignments[0].name}' assignment with '${student.name}' student.")
seedAssignmentSubmission(
submissionSeeds = listOf(SubmissionsApi.SubmissionSeedInfo(
amount = 1,
submissionType = SubmissionType.ONLINE_TEXT_ENTRY
- )),
- assignmentId = assignments[0].id,
- courseId = course.id,
- studentToken = student.token
- )
+ )), assignmentId = assignments[0].id, courseId = course.id, studentToken = student.token)
- Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.")
+ Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.")
tokenLogin(teacher)
dashboardPage.waitForRender()
@@ -83,33 +71,18 @@ class TodoE2ETest : TeacherTest() {
dashboardPage.openTodo()
todoPage.waitForRender()
- Log.d(STEP_TAG,"Assert that the previously seeded ${assignments[0].name} assignment is displayed as a To Do element for the ${course.name} course." +
+ Log.d(STEP_TAG,"Assert that the previously seeded '${assignments[0].name}' assignment is displayed as a To Do element for the '${course.name}' course." +
"Assert that the '1 Needs Grading' text is under the corresponding assignment's details, and assert that the To Do element count is 1.")
todoPage.assertTodoElementDetailsDisplayed(course.name)
todoPage.assertNeedsGradingCountOfTodoElement(assignments[0].name, 1)
todoPage.assertTodoElementCount(1)
- Log.d(PREPARATION_TAG,"Grade the previously seeded submission for ${student.name} student.")
- gradeSubmission(teacher, course, assignments, student)
+ Log.d(PREPARATION_TAG,"Grade the previously seeded submission for '${student.name}' student.")
+ SubmissionsApi.gradeSubmission(teacher.token, course.id, assignments[0].id, student.id, postedGrade = "15")
Log.d(STEP_TAG,"Refresh the To Do Page. Assert that the empty view is displayed so that the To Do has disappeared because it has been graded.")
todoPage.refresh()
todoPage.assertEmptyView()
}
- private fun gradeSubmission(
- teacher: CanvasUserApiModel,
- course: CourseApiModel,
- assignment: List,
- student: CanvasUserApiModel
- ) {
- SubmissionsApi.gradeSubmission(
- teacherToken = teacher.token,
- courseId = course.id,
- assignmentId = assignment[0].id,
- studentId = student.id,
- postedGrade = "15",
- excused = false
- )
- }
}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDetailsPage.kt
index f8a5319490..51a70e4d93 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDetailsPage.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDetailsPage.kt
@@ -17,6 +17,7 @@ package com.instructure.teacher.ui.pages
import androidx.test.InstrumentationRegistry
+import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.web.assertion.WebViewAssertions
import androidx.test.espresso.web.sugar.Web
import androidx.test.espresso.web.webdriver.DriverAtoms
@@ -34,7 +35,7 @@ import org.hamcrest.Matchers
* @constructor Create empty Assignment details page
*/
@Suppress("unused")
-class AssignmentDetailsPage : BasePage(pageResId = R.id.assignmentDetailsPage) {
+class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage(pageResId = R.id.assignmentDetailsPage) {
private val backButton by OnViewWithContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description,false)
private val toolbarTitle by OnViewWithText(R.string.assignment_details)
@@ -132,7 +133,7 @@ class AssignmentDetailsPage : BasePage(pageResId = R.id.assignmentDetailsPage) {
* @param assignment
*/
fun assertAssignmentDetails(assignment: Assignment) {
- assertAssignmentDetails(assignment.name!!, assignment.published)
+ assertAssignmentDetails(assignmentNameTextView, assignment.name!!, assignment.published)
}
/**
@@ -141,7 +142,7 @@ class AssignmentDetailsPage : BasePage(pageResId = R.id.assignmentDetailsPage) {
* @param assignment
*/
fun assertAssignmentDetails(assignment: AssignmentApiModel) {
- assertAssignmentDetails(assignment.name, assignment.published)
+ assertAssignmentDetails(assignmentNameTextView, assignment.name, assignment.published)
}
/**
@@ -333,13 +334,13 @@ class AssignmentDetailsPage : BasePage(pageResId = R.id.assignmentDetailsPage) {
}
/**
- * Assert assignment details
+ * Assert module item details
*
- * @param assignmentName
+ * @param moduleItemName
* @param published
*/
- private fun assertAssignmentDetails(assignmentName: String, published: Boolean) {
- assignmentNameTextView.assertHasText(assignmentName)
+ private fun assertAssignmentDetails(viewInteraction: ViewInteraction, moduleItemName: String, published: Boolean) {
+ viewInteraction.assertHasText(moduleItemName)
assertPublishedStatus(published)
}
}
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt
index 840968381c..89d6564c8f 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt
@@ -37,6 +37,7 @@ import com.instructure.espresso.page.plus
import com.instructure.espresso.page.waitForViewWithText
import com.instructure.espresso.page.withId
import com.instructure.espresso.page.withText
+import com.instructure.espresso.scrollTo
import com.instructure.espresso.swipeDown
import com.instructure.espresso.waitForCheck
import com.instructure.teacher.R
@@ -119,8 +120,7 @@ class CourseBrowserPage : BasePage() {
* Opens the pages tab in the course browser.
*/
fun openPagesTab() {
- scrollDownToCourseBrowser(scrollPosition = magicNumberForScroll)
- waitForViewWithText(R.string.tab_pages).click()
+ waitForViewWithText(R.string.tab_pages).scrollTo().click()
}
/**
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsDetailsPage.kt
index 65f2600f8d..472b6af6ba 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsDetailsPage.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsDetailsPage.kt
@@ -16,6 +16,7 @@
*/
package com.instructure.teacher.ui.pages
+import com.instructure.espresso.ModuleItemInteractions
import com.instructure.espresso.assertDisplayed
import com.instructure.espresso.assertHasText
import com.instructure.espresso.assertNotDisplayed
@@ -28,7 +29,7 @@ import com.instructure.espresso.swipeDown
import com.instructure.teacher.R
import com.instructure.teacher.ui.utils.TypeInRCETextEditor
-class DiscussionsDetailsPage : BasePage() {
+class DiscussionsDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage() {
/**
* Asserts that the discussion has the specified [title].
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt
index b68f2d69f9..6f89f22fd8 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt
@@ -9,9 +9,12 @@ import androidx.test.espresso.web.webdriver.DriverAtoms.getText
import androidx.test.espresso.web.webdriver.Locator
import com.instructure.canvas.espresso.checkToastText
import com.instructure.canvas.espresso.withElementRepeat
+import com.instructure.dataseeding.model.PageApiModel
import com.instructure.espresso.ActivityHelper
+import com.instructure.espresso.ModuleItemInteractions
import com.instructure.espresso.WaitForViewWithId
import com.instructure.espresso.click
+import com.instructure.espresso.extractInnerTextById
import com.instructure.espresso.page.BasePage
import com.instructure.espresso.page.onView
import com.instructure.espresso.replaceText
@@ -27,7 +30,7 @@ import org.hamcrest.Matchers.containsString
*
* @constructor Creates an instance of `EditPageDetailsPage`.
*/
-class EditPageDetailsPage : BasePage() {
+class EditPageDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage() {
private val contentRceView by WaitForViewWithId(R.id.rce_webView)
/**
@@ -104,6 +107,16 @@ class EditPageDetailsPage : BasePage() {
savePage()
checkToastText(R.string.frontPageUnpublishedError, ActivityHelper.currentActivity())
}
+
+ /**
+ * Assert that the page's body is equal to the expected
+ *
+ * @param page The page object to assert.
+ */
+ fun assertPageDetails(page: PageApiModel) {
+ val innerText = extractInnerTextById(page.body, "header1")
+ runTextChecks(WebViewTextCheck(Locator.ID, "header1", innerText!!))
+ }
}
data class WebViewTextCheck(
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt
index 55acb833fc..5bf9682bad 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt
@@ -1,9 +1,12 @@
package com.instructure.teacher.ui.pages
+import androidx.annotation.StringRes
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.withChild
+import com.instructure.canvas.espresso.containsTextCaseInsensitive
import com.instructure.espresso.RecyclerViewItemCountAssertion
import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.assertHasContentDescription
import com.instructure.espresso.assertNotDisplayed
import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
@@ -12,7 +15,9 @@ import com.instructure.espresso.page.plus
import com.instructure.espresso.page.withAncestor
import com.instructure.espresso.page.withDescendant
import com.instructure.espresso.page.withId
+import com.instructure.espresso.page.withParent
import com.instructure.espresso.page.withText
+import com.instructure.espresso.scrollTo
import com.instructure.espresso.swipeDown
import com.instructure.espresso.waitForCheck
import com.instructure.teacher.R
@@ -34,12 +39,9 @@ class ModulesPage : BasePage() {
onView(allOf(withId(R.id.moduleListEmptyView), withAncestor(R.id.moduleList))).assertDisplayed()
}
- /**
- * Asserts that the module is not published.
- */
- fun assertModuleNotPublished() {
- onView(withId(R.id.unpublishedIcon)).assertDisplayed()
- onView(withId(R.id.publishedIcon)).assertNotDisplayed()
+ fun assertModuleNotPublished(moduleTitle: String) {
+ onView(withId(R.id.unpublishedIcon) + withParent(hasSibling(withId(R.id.moduleName) + withText(moduleTitle)))).assertDisplayed()
+ onView(withId(R.id.publishedIcon) + withParent(hasSibling(withId(R.id.moduleName) + withText(moduleTitle)))).assertNotDisplayed()
}
/**
@@ -50,6 +52,11 @@ class ModulesPage : BasePage() {
onView(withId(R.id.publishedIcon)).assertDisplayed()
}
+ fun assertModuleIsPublished(moduleTitle: String) {
+ onView(withId(R.id.unpublishedIcon) + withParent(hasSibling(withId(R.id.moduleName) + withText(moduleTitle)))).assertNotDisplayed()
+ onView(withId(R.id.publishedIcon) + withParent(hasSibling(withId(R.id.moduleName) + withText(moduleTitle)))).assertDisplayed()
+ }
+
/**
* Asserts that the module with the specified title is displayed.
*
@@ -90,21 +97,20 @@ class ModulesPage : BasePage() {
* @param moduleItemName The name of the module item.
*/
fun assertModuleItemIsPublished(moduleItemName: String) {
- val siblingChildMatcher = withChild(withId(R.id.moduleItemTitle) + withText(moduleItemName))
- onView(withId(R.id.moduleItemPublishedIcon) + hasSibling(siblingChildMatcher)).assertDisplayed()
- onView(withId(R.id.moduleItemUnpublishedIcon) + hasSibling(siblingChildMatcher)).assertNotDisplayed()
+ onView(withAncestor(withChild(withText(moduleItemName))) + withId(R.id.moduleItemStatusIcon)).assertHasContentDescription(
+ R.string.a11y_published
+ )
}
/**
* Asserts that the module item with the specified title is not published.
*
- * @param moduleTitle The title of the module.
* @param moduleItemName The name of the module item.
*/
- fun assertModuleItemNotPublished(moduleTitle: String, moduleItemName: String) {
- val siblingChildMatcher = withChild(withId(R.id.moduleItemTitle) + withText(moduleItemName))
- onView(withId(R.id.moduleItemUnpublishedIcon) + hasSibling(siblingChildMatcher)).assertDisplayed()
- onView(withId(R.id.moduleItemPublishedIcon) + hasSibling(siblingChildMatcher)).assertNotDisplayed()
+ fun assertModuleItemNotPublished(moduleItemName: String) {
+ onView(withAncestor(withChild(withText(moduleItemName))) + withId(R.id.moduleItemStatusIcon)).assertHasContentDescription(
+ R.string.a11y_unpublished
+ )
}
/**
@@ -121,7 +127,59 @@ class ModulesPage : BasePage() {
* @param expectedCount The expected item count in the module.
*/
fun assertItemCountInModule(moduleTitle: String, expectedCount: Int) {
- onView(withId(R.id.recyclerView) + withDescendant(withId(R.id.moduleName) +
- withText(moduleTitle))).waitForCheck(RecyclerViewItemCountAssertion(expectedCount + 1)) // Have to increase by one because of the module title element itself.
+ onView(
+ withId(R.id.recyclerView) + withDescendant(
+ withId(R.id.moduleName) +
+ withText(moduleTitle)
+ )
+ ).waitForCheck(RecyclerViewItemCountAssertion(expectedCount + 1)) // Have to increase by one because of the module title element itself.
+ }
+
+ fun assertToolbarMenuItems() {
+ onView(withText(R.string.publishAllModulesAndItems)).assertDisplayed()
+ onView(withText(R.string.publishModulesOnly)).assertDisplayed()
+ onView(withText(R.string.unpublishAllModulesAndItems)).assertDisplayed()
+ }
+
+ fun clickItemOverflow(itemName: String) {
+ onView(withParent(withChild(withText(itemName))) + withId(R.id.publishActions)).scrollTo().click()
+ }
+
+ fun assertModuleMenuItems() {
+ onView(withText(R.string.publishModuleAndItems)).assertDisplayed()
+ onView(withText(R.string.publishModuleOnly)).assertDisplayed()
+ onView(withText(R.string.unpublishModuleAndItems)).assertDisplayed()
+ }
+
+ fun assertOverflowItem(@StringRes title: Int) {
+ onView(withText(title)).assertDisplayed()
+ }
+
+ fun assertFileEditDialogVisible() {
+ onView(withText(R.string.edit_permissions)).assertDisplayed()
+ }
+
+ fun clickOnText(@StringRes title: Int) {
+ onView(withText(title)).click()
+ }
+
+ fun assertSnackbarText(@StringRes snackbarText: Int) {
+ onView(withId(com.google.android.material.R.id.snackbar_text) + withText(snackbarText)).assertDisplayed()
+ }
+
+ fun assertSnackbarContainsText(snackbarText: String) {
+ onView(withId(com.google.android.material.R.id.snackbar_text) + containsTextCaseInsensitive(snackbarText)).assertDisplayed()
+ }
+
+ fun assertModuleItemHidden(moduleItemName: String) {
+ onView(withAncestor(withChild(withText(moduleItemName))) + withId(R.id.moduleItemStatusIcon)).assertHasContentDescription(
+ R.string.a11y_hidden
+ )
+ }
+
+ fun assertModuleItemScheduled(moduleItemName: String) {
+ onView(withAncestor(withChild(withText(moduleItemName))) + withId(R.id.moduleItemStatusIcon)).assertHasContentDescription(
+ R.string.a11y_scheduled
+ )
}
}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProgressPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProgressPage.kt
new file mode 100644
index 0000000000..9a952b34aa
--- /dev/null
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProgressPage.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.instructure.teacher.ui.pages
+
+import androidx.annotation.StringRes
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.getStringFromResource
+
+@OptIn(ExperimentalTestApi::class)
+class ProgressPage(private val composeTestRule: ComposeTestRule) : BasePage() {
+
+ fun clickDone() {
+ composeTestRule.waitForIdle()
+ composeTestRule.waitUntilExactlyOneExists(hasText("Done"), 10000)
+ composeTestRule.onNodeWithText("Done").performClick()
+ }
+
+ fun assertProgressPageTitle(@StringRes title: Int) {
+ composeTestRule.waitUntilExactlyOneExists(hasText(getStringFromResource(title)), 10000)
+ composeTestRule.onNodeWithText(getStringFromResource(title)).assertIsDisplayed()
+ }
+
+ fun assertProgressPageNote(@StringRes note: Int) {
+ composeTestRule.waitUntilExactlyOneExists(hasText(getStringFromResource(note)), 10000)
+ composeTestRule.onNodeWithText(getStringFromResource(note)).assertIsDisplayed()
+ }
+}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizDetailsPage.kt
index 19d6249ab0..7f683cdb4c 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizDetailsPage.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizDetailsPage.kt
@@ -16,8 +16,9 @@
package com.instructure.teacher.ui.pages
import androidx.test.InstrumentationRegistry
-import androidx.test.espresso.matcher.ViewMatchers.withId
import com.instructure.canvasapi2.models.Quiz
+import com.instructure.dataseeding.model.QuizApiModel
+import com.instructure.espresso.ModuleItemInteractions
import com.instructure.espresso.OnViewWithContentDescription
import com.instructure.espresso.OnViewWithId
import com.instructure.espresso.OnViewWithText
@@ -34,6 +35,7 @@ import com.instructure.espresso.page.BasePage
import com.instructure.espresso.page.onView
import com.instructure.espresso.page.scrollTo
import com.instructure.espresso.page.waitForView
+import com.instructure.espresso.page.withId
import com.instructure.espresso.swipeDown
import com.instructure.teacher.R
@@ -46,7 +48,7 @@ import com.instructure.teacher.R
* that can be accessed for performing assertions and interactions. The page has a specific resource ID
* associated with it, which is R.id.quizDetailsPage.
*/
-class QuizDetailsPage : BasePage(pageResId = R.id.quizDetailsPage) {
+class QuizDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage(pageResId = R.id.quizDetailsPage) {
private val backButton by OnViewWithContentDescription(R.string.abc_action_bar_up_description,false)
private val toolbarTitle by OnViewWithText(R.string.quiz_details)
@@ -113,8 +115,27 @@ class QuizDetailsPage : BasePage(pageResId = R.id.quizDetailsPage) {
* @param quiz The Quiz object representing the quiz details.
*/
fun assertQuizDetails(quiz: Quiz) {
- quizTitleTextView.assertHasText(quiz.title!!)
- if (quiz.published) {
+ assertQuizDetails(quiz.title!!, quiz.published)
+ }
+
+ /**
+ * Asserts the quiz details such as title and publish status.
+ *
+ * @param quiz The Quiz object representing the quiz details.
+ */
+ fun assertQuizDetails(quiz: QuizApiModel) {
+ assertQuizDetails(quiz.title, quiz.published)
+ }
+
+ /**
+ * Assert quiz details
+ * Private method used for overloading.
+ * @param quizTitle The quiz's title
+ * @param published The quiz's published status
+ */
+ private fun assertQuizDetails(quizTitle: String, published: Boolean) {
+ quizTitleTextView.assertHasText(quizTitle)
+ if (published) {
publishStatusTextView.assertHasText(R.string.published)
} else {
publishStatusTextView.assertHasText(R.string.not_published)
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/UpdateFilePermissionsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/UpdateFilePermissionsPage.kt
new file mode 100644
index 0000000000..798d03d596
--- /dev/null
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/UpdateFilePermissionsPage.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.instructure.teacher.ui.pages
+
+import com.instructure.espresso.OnViewWithId
+import com.instructure.espresso.assertChecked
+import com.instructure.espresso.assertDisabled
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.assertEnabled
+import com.instructure.espresso.assertHasText
+import com.instructure.espresso.assertNotDisplayed
+import com.instructure.espresso.click
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onViewWithText
+import com.instructure.espresso.page.waitForViewWithId
+import com.instructure.espresso.scrollTo
+import com.instructure.espresso.swipeUp
+import com.instructure.teacher.R
+import java.text.SimpleDateFormat
+import java.util.Date
+
+class UpdateFilePermissionsPage : BasePage() {
+
+ private val saveButton by OnViewWithId(R.id.updateButton)
+ private val publishRadioButton by OnViewWithId(R.id.publish)
+ private val unpublishRadioButton by OnViewWithId(R.id.unpublish)
+ private val hideRadioButton by OnViewWithId(R.id.hide)
+ private val scheduleRadioButton by OnViewWithId(R.id.schedule)
+ private val inheritRadioButton by OnViewWithId(R.id.visibilityInherit)
+ private val contextRadioButton by OnViewWithId(R.id.visibilityContext)
+ private val institutionRadioButton by OnViewWithId(R.id.visibilityInstitution)
+ private val publicRadioButton by OnViewWithId(R.id.visibilityPublic)
+ private val scheduleLayout by OnViewWithId(R.id.scheduleLayout)
+ private val availableFromDate by OnViewWithId(R.id.availableFromDate)
+ private val availableFromTime by OnViewWithId(R.id.availableFromTime)
+ private val availableUntilDate by OnViewWithId(R.id.availableUntilDate)
+ private val availableUntilTime by OnViewWithId(R.id.availableUntilTime)
+
+ fun assertFilePublished() {
+ publishRadioButton.assertChecked()
+ }
+
+ fun assertFileUnpublished() {
+ unpublishRadioButton.assertChecked()
+ }
+
+ fun assertFileHidden() {
+ hideRadioButton.assertChecked()
+ }
+
+ fun assertFileScheduled() {
+ scheduleRadioButton.assertChecked()
+ }
+
+ fun assertFileVisibilityInherit() {
+ inheritRadioButton.assertChecked()
+ }
+
+ fun assertFileVisibilityContext() {
+ contextRadioButton.assertChecked()
+ }
+
+ fun assertFileVisibilityInstitution() {
+ institutionRadioButton.assertChecked()
+ }
+
+ fun assertFileVisibilityPublic() {
+ publicRadioButton.assertChecked()
+ }
+
+ fun clickSaveButton() {
+ saveButton.click()
+ }
+
+ fun clickPublishRadioButton() {
+ waitForViewWithId(R.id.publish).click()
+ }
+
+ fun clickUnpublishRadioButton() {
+ waitForViewWithId(R.id.unpublish).click()
+ }
+
+ fun clickHideRadioButton() {
+ waitForViewWithId(R.id.hide).click()
+ }
+
+ fun assertScheduleLayoutDisplayed() {
+ scheduleLayout.assertDisplayed()
+ }
+
+ fun assertScheduleLayoutNotDisplayed() {
+ scheduleLayout.assertNotDisplayed()
+ }
+
+ fun assertUnlockDate(unlockDate: Date) {
+ val dateString = SimpleDateFormat("MMM d, YYYY").format(unlockDate)
+ val timeString = SimpleDateFormat("h:mm a").format(unlockDate)
+
+ waitForViewWithId(R.id.availableFromDate).scrollTo().assertDisplayed()
+ availableFromDate.assertHasText(dateString)
+ availableFromTime.assertHasText(timeString)
+ }
+
+ fun assertLockDate(lockDate: Date) {
+ val dateString = SimpleDateFormat("MMM d, YYYY").format(lockDate)
+ val timeString = SimpleDateFormat("h:mm a").format(lockDate)
+
+ waitForViewWithId(R.id.availableUntilDate).scrollTo().assertDisplayed()
+ availableUntilDate.assertHasText(dateString)
+ availableUntilTime.assertHasText(timeString)
+ }
+
+ fun assertVisibilityDisabled() {
+ inheritRadioButton.assertDisabled()
+ contextRadioButton.assertDisabled()
+ institutionRadioButton.assertDisabled()
+ publicRadioButton.assertDisabled()
+ }
+
+ fun assertVisibilityEnabled() {
+ inheritRadioButton.assertEnabled()
+ contextRadioButton.assertEnabled()
+ institutionRadioButton.assertEnabled()
+ publicRadioButton.assertEnabled()
+ }
+
+ fun swipeUpBottomSheet() {
+ onViewWithText(R.string.edit_permissions).swipeUp()
+ Thread.sleep(1000)
+ }
+}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/ModuleListRenderTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/ModuleListRenderTest.kt
index 960d9b2744..31bf453bbf 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/ModuleListRenderTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/ModuleListRenderTest.kt
@@ -16,10 +16,12 @@
package com.instructure.teacher.ui.renderTests
import android.graphics.Color
-import android.os.Build
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.ModuleContentDetails
+import com.instructure.canvasapi2.models.ModuleItem
+import com.instructure.canvasapi2.utils.toApiString
import com.instructure.espresso.assertCompletelyDisplayed
import com.instructure.espresso.assertDisplayed
import com.instructure.espresso.assertHasText
@@ -37,6 +39,7 @@ import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Test
+import java.util.Date
@HiltAndroidTest
class ModuleListRenderTest : TeacherRenderTest() {
@@ -51,17 +54,20 @@ class ModuleListRenderTest : TeacherRenderTest() {
id = 1L,
name = "Module 1",
isPublished = true,
+ isLoading = false,
moduleItems = emptyList()
)
moduleItemTemplate = ModuleListItemData.ModuleItemData(
id = 2L,
title = "Assignment Module Item",
subtitle = "Due Tomorrow",
+ subtitle2 = "10 pts",
iconResId = R.drawable.ic_assignment,
isPublished = true,
indent = 0,
tintColor = Color.BLUE,
- enabled = true
+ enabled = true,
+ type = ModuleItem.Type.Assignment
)
}
@@ -86,9 +92,9 @@ class ModuleListRenderTest : TeacherRenderTest() {
fun displaysInlineError() {
val state = ModuleListViewState(
items = listOf(
- ModuleListItemData.ModuleData(1, "Module 1", true, emptyList()),
- ModuleListItemData.ModuleData(2, "Module 2", true, emptyList()),
- ModuleListItemData.ModuleData(3, "Module 3", true, emptyList()),
+ ModuleListItemData.ModuleData(1, "Module 1", true, emptyList(), false),
+ ModuleListItemData.ModuleData(2, "Module 2", true, emptyList(), false),
+ ModuleListItemData.ModuleData(3, "Module 3", true, emptyList(), false),
ModuleListItemData.InlineError(Color.BLUE)
)
)
@@ -107,7 +113,7 @@ class ModuleListRenderTest : TeacherRenderTest() {
@Test
fun displaysEmptyModule() {
- val module = ModuleListItemData.ModuleData(1, "Module 1", true, emptyList())
+ val module = ModuleListItemData.ModuleData(1, "Module 1", true, emptyList(), false)
val state = ModuleListViewState(
items = listOf(module)
)
@@ -128,7 +134,7 @@ class ModuleListRenderTest : TeacherRenderTest() {
fun displaysInlineLoadingView() {
val state = ModuleListViewState(
items = listOf(
- ModuleListItemData.ModuleData(1, "Module 1", true, emptyList()),
+ ModuleListItemData.ModuleData(1, "Module 1", true, emptyList(), false),
ModuleListItemData.Loading
)
)
@@ -156,8 +162,7 @@ class ModuleListRenderTest : TeacherRenderTest() {
items = listOf(moduleItem)
)
loadPageWithViewState(state)
- page.moduleItemPublishedIcon.assertDisplayed()
- page.moduleItemUnpublishedIcon.assertNotDisplayed()
+ page.assertStatusIconContentDescription(R.string.a11y_published)
}
@Test
@@ -169,21 +174,7 @@ class ModuleListRenderTest : TeacherRenderTest() {
items = listOf(moduleItem)
)
loadPageWithViewState(state)
- page.moduleItemUnpublishedIcon.assertDisplayed()
- page.moduleItemPublishedIcon.assertNotDisplayed()
- }
-
- @Test
- fun doesNotDisplayModuleItemPublishStatusIcon() {
- val moduleItem = moduleItemTemplate.copy(
- isPublished = null
- )
- val state = ModuleListViewState(
- items = listOf(moduleItem)
- )
- loadPageWithViewState(state)
- page.moduleItemUnpublishedIcon.assertNotDisplayed()
- page.moduleItemPublishedIcon.assertNotDisplayed()
+ page.assertStatusIconContentDescription(R.string.a11y_unpublished)
}
@Test
@@ -215,13 +206,16 @@ class ModuleListRenderTest : TeacherRenderTest() {
id = idx + 2L,
title = "Module Item ${idx + 1}",
subtitle = null,
+ subtitle2 = null,
iconResId = R.drawable.ic_assignment,
isPublished = false,
+ isLoading = false,
indent = 0,
tintColor = Color.BLUE,
- enabled = true
+ enabled = true,
+ type = ModuleItem.Type.Assignment
)
- }
+ }, false
)
)
)
@@ -308,13 +302,16 @@ class ModuleListRenderTest : TeacherRenderTest() {
id = idx + 2L,
title = "Module Item ${idx + 1}",
subtitle = null,
+ subtitle2 = null,
iconResId = R.drawable.ic_assignment,
isPublished = false,
+ isLoading = false,
indent = 0,
tintColor = Color.BLUE,
- enabled = true
+ enabled = true,
+ type = ModuleItem.Type.Assignment
)
- }
+ }, false
)
),
collapsedModuleIds = setOf(1L)
@@ -327,7 +324,16 @@ class ModuleListRenderTest : TeacherRenderTest() {
fun scrollsToTargetItem() {
val itemCount = 50
val targetItem = ModuleListItemData.ModuleItemData(
- 1234L, "This is the target item", null, R.drawable.ic_attachment, false, 0, Color.BLUE, true
+ 1234L,
+ "This is the target item",
+ null,
+ null,
+ R.drawable.ic_attachment,
+ false,
+ 0,
+ Color.BLUE,
+ true,
+ type = ModuleItem.Type.Assignment
)
val state = ModuleListViewState(
items = listOf(
@@ -339,10 +345,11 @@ class ModuleListRenderTest : TeacherRenderTest() {
} else {
moduleItemTemplate.copy(
id = idx + 2L,
- title = "Module Item ${idx + 1}"
+ title = "Module Item ${idx + 1}",
+ isLoading = false
)
}
- }
+ }, false
)
)
)
@@ -380,6 +387,42 @@ class ModuleListRenderTest : TeacherRenderTest() {
page.moduleItemRoot.check(matches(not(isEnabled())))
}
+ @Test
+ fun displaysFileModuleItemHiddenIcon() {
+ val item = moduleItemTemplate.copy(
+ iconResId = R.drawable.ic_attachment,
+ type = ModuleItem.Type.File,
+ contentDetails = ModuleContentDetails(
+ hidden = true
+ )
+ )
+ val state = ModuleListViewState(
+ items = listOf(item)
+ )
+ loadPageWithViewState(state)
+ page.moduleItemIcon.assertDisplayed()
+ page.assertStatusIconContentDescription(R.string.a11y_hidden)
+ }
+
+ @Test
+ fun displaysFileModuleItemScheduledIcon() {
+ val item = moduleItemTemplate.copy(
+ iconResId = R.drawable.ic_attachment,
+ type = ModuleItem.Type.File,
+ contentDetails = ModuleContentDetails(
+ hidden = false,
+ locked = true,
+ unlockAt = Date().toApiString()
+ )
+ )
+ val state = ModuleListViewState(
+ items = listOf(item)
+ )
+ loadPageWithViewState(state)
+ page.moduleItemIcon.assertDisplayed()
+ page.assertStatusIconContentDescription(R.string.a11y_scheduled)
+ }
+
private fun loadPageWithViewState(
state: ModuleListViewState,
course: Course = Course(name = "Test Course")
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/ModuleListRenderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/ModuleListRenderPage.kt
index ca1832ec17..7fa38d027c 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/ModuleListRenderPage.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/ModuleListRenderPage.kt
@@ -15,10 +15,12 @@
*/
package com.instructure.teacher.ui.renderTests.pages
+import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import com.instructure.espresso.OnViewWithId
import com.instructure.espresso.RecyclerViewItemCountAssertion
import com.instructure.espresso.assertDisplayed
@@ -51,6 +53,7 @@ class ModuleListRenderPage : BasePage(R.id.moduleList) {
val moduleItemTitle by OnViewWithId(R.id.moduleItemTitle)
val moduleItemIndent by OnViewWithId(R.id.moduleItemIndent)
val moduleItemSubtitle by OnViewWithId(R.id.moduleItemSubtitle)
+ val moduleItemStatusIcon by OnViewWithId(R.id.moduleItemStatusIcon)
val moduleItemPublishedIcon by OnViewWithId(R.id.moduleItemPublishedIcon)
val moduleItemUnpublishedIcon by OnViewWithId(R.id.moduleItemUnpublishedIcon)
val moduleItemLoadingView by OnViewWithId(R.id.moduleItemLoadingView)
@@ -76,4 +79,8 @@ class ModuleListRenderPage : BasePage(R.id.moduleList) {
fun assertHasItemIndent(indent: Int) {
moduleItemIndent.check(matches(ViewSizeMatcher.hasWidth(indent)))
}
+
+ fun assertStatusIconContentDescription(@StringRes contentDescription: Int) {
+ moduleItemStatusIcon.check(matches(withContentDescription(contentDescription)))
+ }
}
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt
new file mode 100644
index 0000000000..10679bf125
--- /dev/null
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.instructure.teacher.ui.utils
+
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import com.instructure.teacher.activities.LoginActivity
+import com.instructure.teacher.ui.pages.ProgressPage
+import org.junit.Rule
+
+abstract class TeacherComposeTest : TeacherTest() {
+
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule()
+
+ val progressPage = ProgressPage(composeTestRule)
+}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt
index dd1f97d460..1c15a9bc52 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt
@@ -23,8 +23,11 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
import com.instructure.canvas.espresso.CanvasTest
import com.instructure.espresso.InstructureActivityTestRule
+import com.instructure.espresso.ModuleItemInteractions
import com.instructure.espresso.Searchable
import com.instructure.teacher.BuildConfig
import com.instructure.teacher.R
@@ -84,6 +87,7 @@ import com.instructure.teacher.ui.pages.SpeedGraderQuizSubmissionPage
import com.instructure.teacher.ui.pages.StudentContextPage
import com.instructure.teacher.ui.pages.SyllabusPage
import com.instructure.teacher.ui.pages.TodoPage
+import com.instructure.teacher.ui.pages.UpdateFilePermissionsPage
import com.instructure.teacher.ui.pages.WebViewLoginPage
import dagger.hilt.android.testing.HiltAndroidRule
import instructure.rceditor.RCETextEditor
@@ -99,6 +103,8 @@ abstract class TeacherTest : CanvasTest() {
override val isTesting = BuildConfig.IS_TESTING
+ val device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
@Inject
lateinit var workerFactory: HiltWorkerFactory
@@ -125,7 +131,7 @@ abstract class TeacherTest : CanvasTest() {
val addMessagePage = AddMessagePage()
val announcementsListPage = AnnouncementsListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn))
val assigneeListPage = AssigneeListPage()
- val assignmentDetailsPage = AssignmentDetailsPage()
+ val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next, R.id.previous))
val assignmentDueDatesPage = AssignmentDueDatesPage()
val assignmentListPage = AssignmentListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn))
val assignmentSubmissionListPage = AssignmentSubmissionListPage()
@@ -145,12 +151,12 @@ abstract class TeacherTest : CanvasTest() {
val remoteConfigSettingsPage = RemoteConfigSettingsPage()
val profileSettingsPage = ProfileSettingsPage()
val editProfileSettingsPage = EditProfileSettingsPage()
- val discussionsDetailsPage = DiscussionsDetailsPage()
+ val discussionsDetailsPage = DiscussionsDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next, R.id.previous))
val discussionsListPage = DiscussionsListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn))
val editAnnouncementDetailsPage = EditAnnouncementDetailsPage()
val editAssignmentDetailsPage = EditAssignmentDetailsPage()
val editDiscussionsDetailsPage = EditDiscussionsDetailsPage()
- val editPageDetailsPage = EditPageDetailsPage()
+ val editPageDetailsPage = EditPageDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next, R.id.previous))
val editQuizDetailsPage = EditQuizDetailsPage()
val editSyllabusPage = EditSyllabusPage()
val inboxMessagePage = InboxMessagePage()
@@ -158,12 +164,12 @@ abstract class TeacherTest : CanvasTest() {
val loginFindSchoolPage = LoginFindSchoolPage()
val loginLandingPage = LoginLandingPage()
val loginSignInPage = LoginSignInPage()
- val modulesPage = ModulesPage()
+ val moduleListPage = ModulesPage()
val navDrawerPage = NavDrawerPage()
val notATeacherPage = NotATeacherPage()
val pageListPage = PageListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn))
val peopleListPage = PeopleListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn))
- val quizDetailsPage = QuizDetailsPage()
+ val quizDetailsPage = QuizDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next, R.id.previous))
val quizListPage = QuizListPage(Searchable(R.id.search, R.id.search_src_text, R.id.clearButton, R.id.backButton))
val quizSubmissionListPage = QuizSubmissionListPage()
val speedGraderCommentsPage = SpeedGraderCommentsPage()
@@ -177,6 +183,7 @@ abstract class TeacherTest : CanvasTest() {
val todoPage = TodoPage()
val webViewLoginPage = WebViewLoginPage()
val fileListPage = FileListPage(Searchable(R.id.search, R.id.queryInput, R.id.clearButton, R.id.backButton))
+ val updateFilePermissionsPage = UpdateFilePermissionsPage()
}
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt
index 83aa31f580..37ecc53877 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt
@@ -29,8 +29,29 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.platform.app.InstrumentationRegistry
import com.instructure.canvas.espresso.waitForMatcherWithSleeps
import com.instructure.canvasapi2.models.User
-import com.instructure.dataseeding.api.*
-import com.instructure.dataseeding.model.*
+import com.instructure.dataseeding.api.AssignmentsApi
+import com.instructure.dataseeding.api.ConversationsApi
+import com.instructure.dataseeding.api.CoursesApi
+import com.instructure.dataseeding.api.EnrollmentsApi
+import com.instructure.dataseeding.api.FileUploadsApi
+import com.instructure.dataseeding.api.PagesApi
+import com.instructure.dataseeding.api.QuizzesApi
+import com.instructure.dataseeding.api.SeedApi
+import com.instructure.dataseeding.api.SubmissionsApi
+import com.instructure.dataseeding.api.UserApi
+import com.instructure.dataseeding.model.AssignmentApiModel
+import com.instructure.dataseeding.model.AttachmentApiModel
+import com.instructure.dataseeding.model.CanvasUserApiModel
+import com.instructure.dataseeding.model.ConversationListApiModel
+import com.instructure.dataseeding.model.CourseApiModel
+import com.instructure.dataseeding.model.EnrollmentTypes
+import com.instructure.dataseeding.model.FileType
+import com.instructure.dataseeding.model.FileUploadType
+import com.instructure.dataseeding.model.PageApiModel
+import com.instructure.dataseeding.model.QuizListApiModel
+import com.instructure.dataseeding.model.QuizSubmissionApiModel
+import com.instructure.dataseeding.model.SubmissionApiModel
+import com.instructure.dataseeding.model.SubmissionType
import com.instructure.dataseeding.util.CanvasNetworkAdapter
import com.instructure.dataseeding.util.DataSeedingException
import com.instructure.dataseeding.util.Randomizer
@@ -40,7 +61,11 @@ import com.instructure.teacher.activities.LoginActivity
import com.instructure.teacher.router.RouteMatcher
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers.anyOf
-import java.io.*
+import java.io.BufferedInputStream
+import java.io.DataInputStream
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileWriter
fun TeacherTest.enterDomain(enrollmentType: String = EnrollmentTypes.TEACHER_ENROLLMENT): CanvasUserApiModel {
@@ -251,16 +276,7 @@ fun TeacherTest.seedAssignmentSubmission(
it.attachmentsList.addAll(fileAttachments)
}
- // Seed the submissions
- val submissionRequest = SubmissionsApi.SubmissionSeedRequest(
- assignmentId = assignmentId,
- courseId = courseId,
- studentToken = studentToken,
- submissionSeedsList = submissionSeeds,
- commentSeedsList = commentSeeds
- )
-
- return SubmissionsApi.seedAssignmentSubmission(submissionRequest)
+ return SubmissionsApi.seedAssignmentSubmission(courseId, studentToken, assignmentId, commentSeeds, submissionSeeds)
}
fun TeacherTest.uploadTextFile(courseId: Long, assignmentId: Long, token: String, fileUploadType: FileUploadType): AttachmentApiModel {
diff --git a/apps/teacher/src/main/AndroidManifest.xml b/apps/teacher/src/main/AndroidManifest.xml
index 8ce6e5605d..69124c6abb 100644
--- a/apps/teacher/src/main/AndroidManifest.xml
+++ b/apps/teacher/src/main/AndroidManifest.xml
@@ -97,7 +97,7 @@
android:noHistory="true"
android:theme="@style/LoginFlowTheme.Splash_Teacher"
android:exported="true">
-
+
@@ -107,7 +107,7 @@
android:host="*.instructure.com"
android:scheme="https" />
-
+
@@ -117,7 +117,7 @@
android:host="*.instructure.com"
android:scheme="http" />
-
+
@@ -127,7 +127,7 @@
android:host="*.canvas.net"
android:scheme="https" />
-
+
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt
index a00a2671b8..0e550b3576 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt
@@ -167,7 +167,7 @@ class InitActivity : BasePresenterActivity On Create")
val masqueradingUserId: Long = intent.getLongExtra(Const.QR_CODE_MASQUERADE_ID, 0L)
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/RouteValidatorActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/RouteValidatorActivity.kt
index 68c8415fe0..d9e092bdb9 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/activities/RouteValidatorActivity.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/RouteValidatorActivity.kt
@@ -21,11 +21,13 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Handler
-import androidx.fragment.app.FragmentActivity
import android.view.Window
import android.widget.Toast
+import androidx.fragment.app.FragmentActivity
+import com.instructure.canvasapi2.managers.OAuthManager
import com.instructure.canvasapi2.models.AccountDomain
import com.instructure.canvasapi2.utils.*
+import com.instructure.canvasapi2.utils.weave.apiAsync
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryWeave
import com.instructure.interactions.router.Route
@@ -34,9 +36,11 @@ import com.instructure.interactions.router.RouterParams
import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.loginapi.login.util.QRLogin
import com.instructure.loginapi.login.util.QRLogin.verifySSOLoginUri
+import com.instructure.pandautils.binding.viewBinding
import com.instructure.pandautils.utils.Const
import com.instructure.pandautils.utils.Utils
import com.instructure.teacher.R
+import com.instructure.teacher.databinding.ActivityRouteValidatorBinding
import com.instructure.teacher.fragments.FileListFragment
import com.instructure.teacher.router.RouteMatcher
import com.instructure.teacher.services.FileDownloadService
@@ -45,12 +49,14 @@ import kotlinx.coroutines.Job
class RouteValidatorActivity : FragmentActivity() {
+ private val binding by viewBinding(ActivityRouteValidatorBinding::inflate)
+
private var routeValidatorJob: Job? = null
public override fun onCreate(savedInstanceState: Bundle?) {
requestWindowFeature(Window.FEATURE_NO_TITLE)
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_route_validator)
+ setContentView(binding.root)
val data: Uri? = intent.data
val url: String? = data?.toString()
@@ -83,6 +89,13 @@ class RouteValidatorActivity : FragmentActivity() {
val tokenResponse = QRLogin.performSSOLogin(data, this@RouteValidatorActivity, true)
+ val authResult = apiAsync { OAuthManager.getAuthenticatedSession(ApiPrefs.fullDomain, it) }.await()
+ if (authResult.isSuccess) {
+ authResult.dataOrNull?.sessionUrl?.let {
+ binding.dummyWebView.loadUrl(it)
+ }
+ }
+
// If we have a real user, this is a QR code from a masquerading web user
val intent = if (tokenResponse.realUser != null && tokenResponse.user != null) {
// We need to set the masquerade request to the user (masqueradee), the real user it the admin user currently masquerading
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/EventBusModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/EventBusModule.kt
new file mode 100644
index 0000000000..f021eb0648
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/di/EventBusModule.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.instructure.teacher.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import org.greenrobot.eventbus.EventBus
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class EventBusModule {
+
+ @Provides
+ fun provideEventBus(): EventBus {
+ return EventBus.getDefault()
+ }
+}
+
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/login/TeacherLoginNavigation.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/login/TeacherLoginNavigation.kt
index 5bd968dd79..5a0b7c3a98 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/login/TeacherLoginNavigation.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/login/TeacherLoginNavigation.kt
@@ -17,6 +17,7 @@
package com.instructure.teacher.features.login
import android.content.Intent
+import android.webkit.CookieManager
import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.loginapi.login.LoginNavigation
@@ -35,6 +36,8 @@ class TeacherLoginNavigation(private val activity: FragmentActivity) : LoginNavi
override fun initMainActivityIntent(): Intent {
PushNotificationRegistrationWorker.scheduleJob(activity, ApiPrefs.isMasquerading)
+ CookieManager.getInstance().flush()
+
return SplashActivity.createIntent(activity, activity.intent?.extras)
}
}
\ No newline at end of file
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListEffectHandler.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListEffectHandler.kt
index 1041a022dc..6e54350cc9 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListEffectHandler.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListEffectHandler.kt
@@ -17,10 +17,14 @@
package com.instructure.teacher.features.modules.list
import com.instructure.canvasapi2.CanvasRestAdapter
+import com.instructure.canvasapi2.apis.ModuleAPI
+import com.instructure.canvasapi2.apis.ProgressAPI
+import com.instructure.canvasapi2.builders.RestParams
import com.instructure.canvasapi2.managers.ModuleManager
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.ModuleItem
import com.instructure.canvasapi2.models.ModuleObject
+import com.instructure.canvasapi2.models.Progress
import com.instructure.canvasapi2.utils.APIHelper
import com.instructure.canvasapi2.utils.DataResult
import com.instructure.canvasapi2.utils.Failure
@@ -29,27 +33,68 @@ import com.instructure.canvasapi2.utils.isValid
import com.instructure.canvasapi2.utils.tryOrNull
import com.instructure.canvasapi2.utils.weave.awaitApi
import com.instructure.canvasapi2.utils.weave.awaitApiResponse
+import com.instructure.pandautils.features.progress.ProgressPreferences
+import com.instructure.pandautils.room.appdatabase.daos.ModuleBulkProgressDao
+import com.instructure.pandautils.room.appdatabase.entities.ModuleBulkProgressEntity
+import com.instructure.pandautils.utils.poll
+import com.instructure.pandautils.utils.retry
+import com.instructure.teacher.R
import com.instructure.teacher.features.modules.list.ui.ModuleListView
import com.instructure.teacher.mobius.common.ui.EffectHandler
import kotlinx.coroutines.launch
import retrofit2.Response
-class ModuleListEffectHandler : EffectHandler() {
+class ModuleListEffectHandler(
+ private val moduleApi: ModuleAPI.ModuleInterface,
+ private val progressApi: ProgressAPI.ProgressInterface,
+ private val progressPreferences: ProgressPreferences,
+ private val moduleBulkProgressDao: ModuleBulkProgressDao
+) : EffectHandler() {
override fun accept(effect: ModuleListEffect) {
when (effect) {
is ModuleListEffect.ShowModuleItemDetailView -> {
view?.routeToModuleItem(effect.moduleItem, effect.canvasContext)
}
+
is ModuleListEffect.LoadNextPage -> loadNextPage(
effect.canvasContext,
effect.pageData,
effect.scrollToItemId
)
+
is ModuleListEffect.ScrollToItem -> view?.scrollToItem(effect.moduleItemId)
is ModuleListEffect.MarkModuleExpanded -> {
CollapsedModulesStore.markModuleCollapsed(effect.canvasContext, effect.moduleId, !effect.isExpanded)
}
+
is ModuleListEffect.UpdateModuleItems -> updateModuleItems(effect.canvasContext, effect.items)
+ is ModuleListEffect.BulkUpdateModules -> bulkUpdateModules(
+ effect.canvasContext,
+ effect.moduleIds,
+ effect.affectedIds,
+ effect.action,
+ effect.skipContentTags,
+ allModules = effect.allModules
+ )
+
+ is ModuleListEffect.UpdateModuleItem -> updateModuleItem(
+ effect.canvasContext,
+ effect.moduleId,
+ effect.itemId,
+ effect.published
+ )
+
+ is ModuleListEffect.ShowSnackbar -> {
+ view?.showSnackbar(effect.message, effect.params)
+ }
+
+ is ModuleListEffect.UpdateFileModuleItem -> {
+ view?.showUpdateFileDialog(effect.fileId, effect.contentDetails)
+ }
+
+ is ModuleListEffect.BulkUpdateStarted -> {
+ handleBulkUpdate(effect.progressId, effect.allModules, effect.skipContentTags, effect.action)
+ }
}.exhaustive
}
@@ -130,9 +175,11 @@ class ModuleListEffectHandler : EffectHandler awaitApiResponse {
ModuleManager.getFirstPageModulesWithItems(canvasContext, it, pageData.forceNetwork)
}
+
pageData.nextPageUrl.isValid() -> awaitApiResponse {
ModuleManager.getNextPageModuleObjects(pageData.nextPageUrl, it, pageData.forceNetwork)
}
+
else -> throw IllegalStateException("Unable to fetch page data; invalid nextPageUrl")
}
@@ -154,4 +201,142 @@ class ModuleListEffectHandler : EffectHandler,
+ affectedIds: List,
+ action: BulkModuleUpdateAction,
+ skipContentTags: Boolean,
+ async: Boolean = true,
+ allModules: Boolean
+ ) {
+ launch {
+ val restParams = RestParams(
+ canvasContext = canvasContext,
+ isForceReadFromNetwork = true
+ )
+ val progress = moduleApi.bulkUpdateModules(
+ canvasContext.type.apiString,
+ canvasContext.id,
+ moduleIds,
+ action.event,
+ skipContentTags,
+ async,
+ restParams
+ ).dataOrNull?.progress
+
+ val bulkUpdateProgress = progress?.progress
+ if (bulkUpdateProgress == null) {
+ consumer.accept(ModuleListEvent.BulkUpdateFailed(skipContentTags))
+ } else {
+ moduleBulkProgressDao.insert(
+ ModuleBulkProgressEntity(
+ courseId = canvasContext.id,
+ progressId = bulkUpdateProgress.id,
+ action = action.toString(),
+ skipContentTags = skipContentTags,
+ allModules = allModules,
+ affectedIds = affectedIds
+ )
+ )
+ if (allModules || !skipContentTags) {
+ showProgressScreen(bulkUpdateProgress.id, skipContentTags, action, allModules)
+ }
+ consumer.accept(
+ ModuleListEvent.BulkUpdateStarted(
+ canvasContext,
+ bulkUpdateProgress.id,
+ allModules,
+ skipContentTags,
+ affectedIds,
+ action
+ )
+ )
+ }
+ }
+ }
+
+ private fun handleBulkUpdate(
+ progressId: Long,
+ allModules: Boolean,
+ skipContentTags: Boolean,
+ action: BulkModuleUpdateAction
+ ) {
+ launch {
+ val success = trackUpdateProgress(progressId)
+ moduleBulkProgressDao.deleteById(progressId)
+
+ if (success) {
+ consumer.accept(ModuleListEvent.BulkUpdateSuccess(skipContentTags, action, allModules))
+ } else {
+ if (progressPreferences.cancelledProgressIds.contains(progressId)) {
+ consumer.accept(ModuleListEvent.BulkUpdateCancelled)
+ progressPreferences.cancelledProgressIds = progressPreferences.cancelledProgressIds - progressId
+ } else {
+ consumer.accept(ModuleListEvent.BulkUpdateFailed(skipContentTags))
+ }
+ }
+ }
+ }
+
+ private suspend fun trackUpdateProgress(progressId: Long): Boolean {
+ val params = RestParams(isForceReadFromNetwork = true)
+
+ val result = poll(500, maxAttempts = -1,
+ validate = {
+ it.hasRun
+ },
+ block = {
+ var newProgress: Progress? = null
+ retry(initialDelay = 500) {
+ newProgress = progressApi.getProgress(progressId.toString(), params).dataOrThrow
+ }
+ newProgress
+ })
+
+ return result?.hasRun == true && result.isCompleted
+ }
+
+ private fun updateModuleItem(canvasContext: CanvasContext, moduleId: Long, itemId: Long, published: Boolean) {
+ launch {
+ val restParams = RestParams(
+ canvasContext = canvasContext,
+ isForceReadFromNetwork = true
+ )
+ val moduleItem = moduleApi.publishModuleItem(
+ canvasContext.type.apiString,
+ canvasContext.id,
+ moduleId,
+ itemId,
+ published,
+ restParams
+ ).dataOrNull
+
+ moduleItem?.let {
+ consumer.accept(ModuleListEvent.ModuleItemUpdateSuccess(it, published))
+ } ?: consumer.accept(ModuleListEvent.ModuleItemUpdateFailed(itemId))
+ }
+ }
+
+ private fun showProgressScreen(
+ progressId: Long,
+ skipContentTags: Boolean,
+ action: BulkModuleUpdateAction,
+ allModules: Boolean
+ ) {
+ val title = when {
+ allModules && skipContentTags -> R.string.allModules
+ allModules && !skipContentTags -> R.string.allModulesAndItems
+ !allModules && !skipContentTags -> R.string.selectedModulesAndItems
+ else -> R.string.selectedModules
+ }
+
+ val progressTitle = when (action) {
+ BulkModuleUpdateAction.PUBLISH -> R.string.publishing
+ BulkModuleUpdateAction.UNPUBLISH -> R.string.unpublishing
+ }
+
+ view?.showProgressDialog(progressId, title, progressTitle, R.string.moduleBulkUpdateNote)
+ }
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListModels.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListModels.kt
index 221d3ede60..4614f652db 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListModels.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListModels.kt
@@ -16,7 +16,9 @@
*/
package com.instructure.teacher.features.modules.list
+import androidx.annotation.StringRes
import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.ModuleContentDetails
import com.instructure.canvasapi2.models.ModuleItem
import com.instructure.canvasapi2.models.ModuleObject
import com.instructure.canvasapi2.utils.DataResult
@@ -32,6 +34,34 @@ sealed class ModuleListEvent {
data class ItemRefreshRequested(val type: String, val predicate: (item: ModuleItem) -> Boolean) : ModuleListEvent()
data class ReplaceModuleItems(val items: List) : ModuleListEvent()
data class RemoveModuleItems(val type: String, val predicate: (item: ModuleItem) -> Boolean) : ModuleListEvent()
+ data class BulkUpdateModule(val moduleId: Long, val action: BulkModuleUpdateAction, val skipContentTags: Boolean) :
+ ModuleListEvent()
+
+ data class BulkUpdateAllModules(val action: BulkModuleUpdateAction, val skipContentTags: Boolean) :
+ ModuleListEvent()
+
+ data class UpdateModuleItem(val itemId: Long, val isPublished: Boolean) : ModuleListEvent()
+ data class ModuleItemUpdateSuccess(val item: ModuleItem, val published: Boolean) : ModuleListEvent()
+ data class ModuleItemUpdateFailed(val itemId: Long) : ModuleListEvent()
+ data class BulkUpdateSuccess(
+ val skipContentTags: Boolean,
+ val action: BulkModuleUpdateAction,
+ val allModules: Boolean
+ ) : ModuleListEvent()
+
+ data class BulkUpdateFailed(val skipContentTags: Boolean) : ModuleListEvent()
+ data class BulkUpdateStarted(
+ val canvasContext: CanvasContext,
+ val progressId: Long,
+ val allModules: Boolean,
+ val skipContentTags: Boolean,
+ val affectedIds: List,
+ val action: BulkModuleUpdateAction
+ ) : ModuleListEvent()
+
+ data class UpdateFileModuleItem(val fileId: Long, val contentDetails: ModuleContentDetails) : ModuleListEvent()
+ object BulkUpdateCancelled : ModuleListEvent()
+ data class ShowSnackbar(@StringRes val message: Int, val params: Array = emptyArray()): ModuleListEvent()
}
sealed class ModuleListEffect {
@@ -39,18 +69,69 @@ sealed class ModuleListEffect {
val moduleItem: ModuleItem,
val canvasContext: CanvasContext
) : ModuleListEffect()
+
data class LoadNextPage(
val canvasContext: CanvasContext,
val pageData: ModuleListPageData,
val scrollToItemId: Long?
) : ModuleListEffect()
+
data class ScrollToItem(val moduleItemId: Long) : ModuleListEffect()
data class MarkModuleExpanded(
val canvasContext: CanvasContext,
val moduleId: Long,
val isExpanded: Boolean
) : ModuleListEffect()
+
data class UpdateModuleItems(val canvasContext: CanvasContext, val items: List) : ModuleListEffect()
+
+ data class BulkUpdateModules(
+ val canvasContext: CanvasContext,
+ val moduleIds: List,
+ val affectedIds: List,
+ val action: BulkModuleUpdateAction,
+ val skipContentTags: Boolean,
+ val allModules: Boolean
+ ) : ModuleListEffect()
+
+ data class UpdateModuleItem(
+ val canvasContext: CanvasContext,
+ val moduleId: Long,
+ val itemId: Long,
+ val published: Boolean
+ ) : ModuleListEffect()
+
+ data class ShowSnackbar(@StringRes val message: Int, val params: Array = emptyArray()) : ModuleListEffect() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ShowSnackbar
+
+ if (message != other.message) return false
+ if (!params.contentEquals(other.params)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = message
+ result = 31 * result + params.contentHashCode()
+ return result
+ }
+ }
+
+ data class UpdateFileModuleItem(
+ val fileId: Long,
+ val contentDetails: ModuleContentDetails
+ ) : ModuleListEffect()
+
+ data class BulkUpdateStarted(
+ val progressId: Long,
+ val allModules: Boolean,
+ val skipContentTags: Boolean,
+ val action: BulkModuleUpdateAction
+ ) : ModuleListEffect()
}
data class ModuleListModel(
@@ -70,3 +151,8 @@ data class ModuleListPageData(
val isFirstPage get() = lastPageResult == null
val hasMorePages get() = isFirstPage || nextPageUrl.isValid()
}
+
+enum class BulkModuleUpdateAction(val event: String) {
+ PUBLISH("publish"),
+ UNPUBLISH("unpublish")
+}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListPresenter.kt
index 9526c732d4..0aca97e00a 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListPresenter.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListPresenter.kt
@@ -42,18 +42,22 @@ object ModuleListPresenter : Presenter {
val moduleItems: List = if (module.items.isNotEmpty()) {
module.items.map { item ->
if (item.type.equals(ModuleItem.Type.SubHeader.name, ignoreCase = true)) {
- ModuleListItemData.ModuleItemData(
- id = item.id,
- title = null,
- subtitle = item.title,
- iconResId = null,
- isPublished = item.published,
- indent = item.indent * indentWidth,
- tintColor = 0,
- enabled = false
+ ModuleListItemData.SubHeader(
+ id = item.id,
+ title = item.title,
+ indent = item.indent * indentWidth,
+ enabled = false,
+ published = item.published,
+ isLoading = item.id in model.loadingModuleItemIds
)
} else {
- createModuleItemData(item, context, indentWidth, iconTint, item.id in model.loadingModuleItemIds)
+ createModuleItemData(
+ item,
+ context,
+ indentWidth,
+ iconTint,
+ item.id in model.loadingModuleItemIds
+ )
}
}
} else {
@@ -63,7 +67,8 @@ object ModuleListPresenter : Presenter {
id = module.id,
name = module.name.orEmpty(),
isPublished = module.published,
- moduleItems = moduleItems
+ moduleItems = moduleItems,
+ isLoading = module.id in model.loadingModuleItemIds
)
}
@@ -98,12 +103,13 @@ object ModuleListPresenter : Presenter {
loading: Boolean
): ModuleListItemData.ModuleItemData {
val subtitle = item.moduleDetails?.dueDate?.let {
- context.getString(
- R.string.due,
- DateHelper.getMonthDayTimeMaybeMinutesMaybeYear(context, it, R.string.at)
- )
+ DateHelper.getMonthDayTimeMaybeMinutesMaybeYear(context, it, R.string.at)
}
+ val pointsPossible = item.moduleDetails?.pointsPossible?.toFloatOrNull()
+ val subtitle2 =
+ pointsPossible?.let { context.resources.getQuantityString(R.plurals.moduleItemPoints, it.toInt(), it) }
+
val iconRes: Int? = when (tryOrNull { ModuleItem.Type.valueOf(item.type.orEmpty()) }) {
ModuleItem.Type.Assignment -> R.drawable.ic_assignment
ModuleItem.Type.Discussion -> R.drawable.ic_discussion
@@ -119,12 +125,17 @@ object ModuleListPresenter : Presenter {
id = item.id,
title = item.title,
subtitle = subtitle,
- iconResId = iconRes.takeUnless { loading },
+ subtitle2 = subtitle2,
+ iconResId = iconRes,
isPublished = item.published,
indent = item.indent * indentWidth,
tintColor = courseColor,
enabled = !loading,
- isLoading = loading
+ isLoading = loading,
+ type = tryOrNull { ModuleItem.Type.valueOf(item.type.orEmpty()) } ?: ModuleItem.Type.Assignment,
+ contentDetails = item.moduleDetails,
+ contentId = item.contentId,
+ unpublishable = item.unpublishable
)
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListUpdate.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListUpdate.kt
index 1087454574..d01a85f6fd 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListUpdate.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ModuleListUpdate.kt
@@ -16,8 +16,10 @@
*/
package com.instructure.teacher.features.modules.list
+import androidx.annotation.StringRes
import com.instructure.canvasapi2.CanvasRestAdapter
import com.instructure.canvasapi2.utils.patchedBy
+import com.instructure.teacher.R
import com.instructure.teacher.mobius.common.ui.UpdateInit
import com.spotify.mobius.First
import com.spotify.mobius.Next
@@ -52,10 +54,12 @@ class ModuleListUpdate : UpdateInit {
val item = model.modules.flatMap { it.items }.first { it.id == event.moduleItemId }
return Next.dispatch(setOf(ModuleListEffect.ShowModuleItemDetailView(item, model.course)))
}
+
is ModuleListEvent.PageLoaded -> {
val effects = mutableSetOf()
var newModel = model.copy(
@@ -67,7 +71,8 @@ class ModuleListUpdate : UpdateInit module.items.any { it.id == model.scrollToItemId } }) {
+ && newModules.any { module -> module.items.any { it.id == model.scrollToItemId } }
+ ) {
newModel = newModel.copy(scrollToItemId = null)
effects += ModuleListEffect.ScrollToItem(model.scrollToItemId)
}
@@ -75,6 +80,7 @@ class ModuleListUpdate : UpdateInit {
return if (model.isLoading || !model.pageData.hasMorePages) {
// Do nothing if we're already loading or all pages have loaded
@@ -89,13 +95,19 @@ class ModuleListUpdate : UpdateInit {
- return Next.dispatch(setOf(ModuleListEffect.MarkModuleExpanded(
- model.course,
- event.moduleId,
- event.isExpanded
- )))
+ return Next.dispatch(
+ setOf(
+ ModuleListEffect.MarkModuleExpanded(
+ model.course,
+ event.moduleId,
+ event.isExpanded
+ )
+ )
+ )
}
+
is ModuleListEvent.ModuleItemLoadStatusChanged -> {
return Next.next(
model.copy(
@@ -107,6 +119,7 @@ class ModuleListUpdate : UpdateInit {
val items = model.modules.flatMap { it.items }.filter { it.type == event.type }.filter(event.predicate)
return if (items.isEmpty()) {
@@ -116,6 +129,7 @@ class ModuleListUpdate : UpdateInit {
val itemGroups = event.items.groupBy { it.moduleId }
val newModel = model.copy(
@@ -130,6 +144,7 @@ class ModuleListUpdate : UpdateInit {
val newModel = model.copy(
modules = model.modules.map { module ->
@@ -141,6 +156,191 @@ class ModuleListUpdate : UpdateInit {
+ val affectedIds = mutableListOf(event.moduleId)
+ if (!event.skipContentTags) {
+ affectedIds.addAll(model.modules.filter { it.id == event.moduleId }
+ .flatMap { it.items }
+ .map { it.id })
+ }
+
+ val newModel = model.copy(
+ loadingModuleItemIds = model.loadingModuleItemIds + affectedIds
+ )
+ val effect = ModuleListEffect.BulkUpdateModules(
+ model.course,
+ listOf(event.moduleId),
+ affectedIds,
+ event.action,
+ event.skipContentTags,
+ false
+ )
+ return Next.next(newModel, setOf(effect))
+ }
+
+ is ModuleListEvent.BulkUpdateAllModules -> {
+ val affectedIds = mutableListOf()
+ affectedIds.addAll(model.modules.map { it.id })
+ if (!event.skipContentTags) {
+ affectedIds.addAll(model.modules.flatMap { it.items }.map { it.id })
+ }
+
+ val newModel = model.copy(
+ loadingModuleItemIds = model.loadingModuleItemIds + affectedIds
+ )
+ val effect = ModuleListEffect.BulkUpdateModules(
+ model.course,
+ model.modules.map { it.id },
+ affectedIds,
+ event.action,
+ event.skipContentTags,
+ true
+ )
+ return Next.next(newModel, setOf(effect))
+ }
+
+ is ModuleListEvent.BulkUpdateSuccess -> {
+ val newModel = model.copy(
+ isLoading = true,
+ modules = emptyList(),
+ pageData = ModuleListPageData(forceNetwork = true),
+ loadingModuleItemIds = emptySet()
+ )
+ val effect = ModuleListEffect.LoadNextPage(
+ newModel.course,
+ newModel.pageData,
+ newModel.scrollToItemId
+ )
+
+ val message = getBulkUpdateSnackbarMessage(event.action, event.skipContentTags, event.allModules)
+
+ val snackbarEffect = ModuleListEffect.ShowSnackbar(message)
+
+ return Next.next(newModel, setOf(effect, snackbarEffect))
+ }
+
+ is ModuleListEvent.BulkUpdateFailed -> {
+ val newModel = model.copy(
+ loadingModuleItemIds = emptySet()
+ )
+
+ val snackbarEffect = ModuleListEffect.ShowSnackbar(R.string.errorOccurred)
+
+ return Next.next(newModel, setOf(snackbarEffect))
+ }
+
+ is ModuleListEvent.UpdateModuleItem -> {
+ val newModel = model.copy(
+ loadingModuleItemIds = model.loadingModuleItemIds + event.itemId
+ )
+ val effect = ModuleListEffect.UpdateModuleItem(
+ model.course,
+ model.modules.first { it.items.any { it.id == event.itemId } }.id,
+ event.itemId,
+ event.isPublished
+ )
+ return Next.next(newModel, setOf(effect))
+ }
+
+ is ModuleListEvent.ModuleItemUpdateSuccess -> {
+ val newModel = model.copy(
+ modules = model.modules.map { module ->
+ if (event.item.moduleId == module.id) {
+ module.copy(items = module.items.patchedBy(listOf(event.item)) { it.id })
+ } else {
+ module
+ }
+ },
+ loadingModuleItemIds = model.loadingModuleItemIds - event.item.id
+ )
+
+ val snackbarEffect =
+ ModuleListEffect.ShowSnackbar(if (event.item.published == true) R.string.moduleItemPublished else R.string.moduleItemUnpublished)
+
+ return Next.next(newModel, setOf(snackbarEffect))
+ }
+
+ is ModuleListEvent.ModuleItemUpdateFailed -> {
+ val newModel = model.copy(
+ loadingModuleItemIds = model.loadingModuleItemIds - event.itemId
+ )
+
+ val snackbarEffect = ModuleListEffect.ShowSnackbar(R.string.errorOccurred)
+
+ return Next.next(newModel, setOf(snackbarEffect))
+ }
+
+ is ModuleListEvent.UpdateFileModuleItem -> {
+ val effect = ModuleListEffect.UpdateFileModuleItem(
+ event.fileId,
+ event.contentDetails
+ )
+ return Next.dispatch(setOf(effect))
+ }
+
+ is ModuleListEvent.BulkUpdateCancelled -> {
+ val newModel = model.copy(
+ isLoading = true,
+ modules = emptyList(),
+ pageData = ModuleListPageData(forceNetwork = true),
+ loadingModuleItemIds = emptySet()
+ )
+ val effect = ModuleListEffect.LoadNextPage(
+ newModel.course,
+ newModel.pageData,
+ newModel.scrollToItemId
+ )
+ val snackbarEffect = ModuleListEffect.ShowSnackbar(R.string.updateCancelled)
+ return Next.next(newModel, setOf(effect, snackbarEffect))
+ }
+
+ is ModuleListEvent.BulkUpdateStarted -> {
+ val newModel = model.copy(
+ loadingModuleItemIds = model.loadingModuleItemIds + event.affectedIds
+ )
+ val effect = ModuleListEffect.BulkUpdateStarted(
+ event.progressId,
+ event.allModules,
+ event.skipContentTags,
+ event.action
+ )
+ return Next.next(newModel, setOf(effect))
+ }
+
+ is ModuleListEvent.ShowSnackbar -> {
+ val effect = ModuleListEffect.ShowSnackbar(event.message, event.params)
+ return Next.dispatch(setOf(effect))
+ }
+ }
+ }
+
+ @StringRes
+ private fun getBulkUpdateSnackbarMessage(
+ action: BulkModuleUpdateAction,
+ skipContentTags: Boolean,
+ allModules: Boolean
+ ): Int {
+ return if (allModules) {
+ if (action == BulkModuleUpdateAction.PUBLISH) {
+ if (skipContentTags) {
+ R.string.onlyModulesPublished
+ } else {
+ R.string.allModulesAndAllItemsPublished
+ }
+ } else {
+ R.string.allModulesAndAllItemsUnpublished
+ }
+ } else {
+ if (action == BulkModuleUpdateAction.PUBLISH) {
+ if (skipContentTags) {
+ R.string.onlyModulePublished
+ } else {
+ R.string.moduleAndAllItemsPublished
+ }
+ } else {
+ R.string.moduleAndAllItemsUnpublished
+ }
}
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListFragment.kt
index 4c4358f49b..7c43150dbb 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListFragment.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListFragment.kt
@@ -1,57 +1,67 @@
/*
- * Copyright (C) 2019 - present Instructure, Inc.
+ * Copyright (C) 2024 - 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.
+ * 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
*
- * 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.
+ * 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.
*
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
*
*/
+
package com.instructure.teacher.features.modules.list.ui
import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.ViewGroup
+import android.view.View
+import androidx.lifecycle.lifecycleScope
+import com.instructure.canvasapi2.apis.ModuleAPI
+import com.instructure.canvasapi2.apis.ProgressAPI
import com.instructure.canvasapi2.models.CanvasContext
-import com.instructure.canvasapi2.utils.pageview.PageView
-import com.instructure.pandautils.analytics.SCREEN_VIEW_MODULE_LIST
-import com.instructure.pandautils.analytics.ScreenView
+import com.instructure.pandautils.features.progress.ProgressPreferences
+import com.instructure.pandautils.room.appdatabase.daos.ModuleBulkProgressDao
import com.instructure.pandautils.utils.Const
-import com.instructure.pandautils.utils.NLongArg
-import com.instructure.pandautils.utils.ParcelableArg
import com.instructure.pandautils.utils.withArgs
-import com.instructure.teacher.databinding.FragmentModuleListBinding
-import com.instructure.teacher.features.modules.list.*
-import com.instructure.teacher.mobius.common.ui.MobiusFragment
-import com.instructure.teacher.mobius.common.ui.Presenter
+import com.instructure.teacher.features.modules.list.ModuleListEffectHandler
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+import javax.inject.Inject
-@PageView(url = "{canvasContext}/modules")
-@ScreenView(SCREEN_VIEW_MODULE_LIST)
-class ModuleListFragment : MobiusFragment() {
+@AndroidEntryPoint
+class ModuleListFragment : ModuleListMobiusFragment() {
- val canvasContext by ParcelableArg(key = Const.COURSE)
+ @Inject
+ lateinit var moduleApi: ModuleAPI.ModuleInterface
- private val scrollToItemId by NLongArg(key = Const.MODULE_ITEM_ID)
+ @Inject
+ lateinit var progressApi: ProgressAPI.ProgressInterface
- override fun makeEffectHandler() = ModuleListEffectHandler()
+ @Inject
+ lateinit var progressPreferences: ProgressPreferences
- override fun makeUpdate() = ModuleListUpdate()
+ @Inject
+ lateinit var moduleBulkProgressDao: ModuleBulkProgressDao
- override fun makeView(inflater: LayoutInflater, parent: ViewGroup) = ModuleListView(inflater, parent, canvasContext)
+ override fun makeEffectHandler() = ModuleListEffectHandler(moduleApi, progressApi, progressPreferences, moduleBulkProgressDao)
- override fun makePresenter(): Presenter = ModuleListPresenter
-
- override fun makeInitModel(): ModuleListModel = ModuleListModel(course = canvasContext, scrollToItemId = scrollToItemId)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ retainInstance = false
+ }
- override val eventSources = listOf(ModuleListEventBusSource())
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ lifecycleScope.launch {
+ val progresses = moduleBulkProgressDao.findByCourseId(canvasContext.id)
+ this@ModuleListFragment.view.bulkUpdateInProgress(progresses)
+ }
+ }
companion object {
@@ -63,5 +73,4 @@ class ModuleListFragment : MobiusFragment.
+ *
+ */
+package com.instructure.teacher.features.modules.list.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.instructure.canvasapi2.apis.ModuleAPI
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.utils.pageview.PageView
+import com.instructure.pandautils.analytics.SCREEN_VIEW_MODULE_LIST
+import com.instructure.pandautils.analytics.ScreenView
+import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.NLongArg
+import com.instructure.pandautils.utils.ParcelableArg
+import com.instructure.pandautils.utils.withArgs
+import com.instructure.teacher.databinding.FragmentModuleListBinding
+import com.instructure.teacher.features.modules.list.*
+import com.instructure.teacher.mobius.common.ui.MobiusFragment
+import com.instructure.teacher.mobius.common.ui.Presenter
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@PageView(url = "{canvasContext}/modules")
+@ScreenView(SCREEN_VIEW_MODULE_LIST)
+abstract class ModuleListMobiusFragment : MobiusFragment() {
+
+ val canvasContext by ParcelableArg(key = Const.COURSE)
+
+ private val scrollToItemId by NLongArg(key = Const.MODULE_ITEM_ID)
+
+ override fun makeUpdate() = ModuleListUpdate()
+
+ override fun makeView(inflater: LayoutInflater, parent: ViewGroup) = ModuleListView(inflater, parent, canvasContext)
+
+ override fun makePresenter(): Presenter = ModuleListPresenter
+
+ override fun makeInitModel(): ModuleListModel = ModuleListModel(course = canvasContext, scrollToItemId = scrollToItemId)
+
+ override val eventSources = listOf(ModuleListEventBusSource())
+
+
+
+}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListRecyclerAdapter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListRecyclerAdapter.kt
index 5e8e6b20f5..013ad27abd 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListRecyclerAdapter.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListRecyclerAdapter.kt
@@ -17,15 +17,31 @@
package com.instructure.teacher.features.modules.list.ui
import android.content.Context
+import androidx.annotation.StringRes
+import com.instructure.canvasapi2.models.ModuleContentDetails
import com.instructure.teacher.adapters.GroupedRecyclerAdapter
import com.instructure.teacher.adapters.ListItemCallback
-import com.instructure.teacher.features.modules.list.ui.binders.*
+import com.instructure.teacher.features.modules.list.ui.binders.ModuleListEmptyBinder
+import com.instructure.teacher.features.modules.list.ui.binders.ModuleListEmptyItemBinder
+import com.instructure.teacher.features.modules.list.ui.binders.ModuleListFullErrorBinder
+import com.instructure.teacher.features.modules.list.ui.binders.ModuleListInlineErrorBinder
+import com.instructure.teacher.features.modules.list.ui.binders.ModuleListItemBinder
+import com.instructure.teacher.features.modules.list.ui.binders.ModuleListLoadingBinder
+import com.instructure.teacher.features.modules.list.ui.binders.ModuleListModuleBinder
+import com.instructure.teacher.features.modules.list.ui.binders.ModuleListSubHeaderBinder
interface ModuleListCallback : ListItemCallback {
fun retryNextPage()
fun moduleItemClicked(moduleItemId: Long)
fun markModuleExpanded(moduleId: Long, isExpanded: Boolean)
+ fun updateModuleItem(itemId: Long, isPublished: Boolean)
+ fun publishModule(moduleId: Long)
+ fun publishModuleAndItems(moduleId: Long)
+ fun unpublishModuleAndItems(moduleId: Long)
+ fun updateFileModuleItem(fileId: Long, contentDetails: ModuleContentDetails)
+
+ fun showSnackbar(@StringRes message: Int, params: Array)
}
class ModuleListRecyclerAdapter(
@@ -49,6 +65,7 @@ class ModuleListRecyclerAdapter(
register(ModuleListItemBinder())
register(ModuleListLoadingBinder())
register(ModuleListEmptyItemBinder())
+ register(ModuleListSubHeaderBinder())
}
fun setData(items: List, collapsedModuleIds: Set) {
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListView.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListView.kt
index c635d19b38..f8c963585c 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListView.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListView.kt
@@ -18,14 +18,24 @@ package com.instructure.teacher.features.modules.list.ui
import android.view.LayoutInflater
import android.view.ViewGroup
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.snackbar.Snackbar
import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.ModuleContentDetails
import com.instructure.canvasapi2.models.ModuleItem
import com.instructure.pandarecycler.PaginatedScrollListener
+import com.instructure.pandautils.features.progress.ProgressDialogFragment
+import com.instructure.pandautils.room.appdatabase.entities.ModuleBulkProgressEntity
import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.pandautils.utils.showThemed
+import com.instructure.teacher.R
import com.instructure.teacher.databinding.FragmentModuleListBinding
+import com.instructure.teacher.features.modules.list.BulkModuleUpdateAction
import com.instructure.teacher.features.modules.list.ModuleListEvent
+import com.instructure.teacher.features.modules.list.ui.file.UpdateFileDialogFragment
import com.instructure.teacher.features.modules.progression.ModuleProgressionFragment
import com.instructure.teacher.mobius.common.ui.MobiusView
import com.instructure.teacher.router.RouteMatcher
@@ -36,7 +46,11 @@ class ModuleListView(
inflater: LayoutInflater,
parent: ViewGroup,
val course: CanvasContext
-) : MobiusView(inflater, FragmentModuleListBinding::inflate, parent) {
+) : MobiusView(
+ inflater,
+ FragmentModuleListBinding::inflate,
+ parent
+) {
private var consumer: Consumer? = null
@@ -59,6 +73,62 @@ class ModuleListView(
consumer?.accept(ModuleListEvent.ModuleExpanded(moduleId, isExpanded))
}
+ override fun publishModule(moduleId: Long) {
+ showConfirmationDialog(
+ R.string.publishDialogTitle,
+ R.string.publishModuleDialogMessage,
+ R.string.publishDialogPositiveButton,
+ R.string.cancel
+ ) {
+ consumer?.accept(ModuleListEvent.BulkUpdateModule(moduleId, BulkModuleUpdateAction.PUBLISH, true))
+ }
+ }
+
+ override fun publishModuleAndItems(moduleId: Long) {
+ showConfirmationDialog(
+ R.string.publishDialogTitle,
+ R.string.publishModuleAndItemsDialogMessage,
+ R.string.publishDialogPositiveButton,
+ R.string.cancel
+ ) {
+ consumer?.accept(ModuleListEvent.BulkUpdateModule(moduleId, BulkModuleUpdateAction.PUBLISH, false))
+ }
+ }
+
+ override fun unpublishModuleAndItems(moduleId: Long) {
+ showConfirmationDialog(
+ R.string.unpublishDialogTitle,
+ R.string.unpublishModuleAndItemsDialogMessage,
+ R.string.unpublishDialogPositiveButton,
+ R.string.cancel
+ ) {
+ consumer?.accept(ModuleListEvent.BulkUpdateModule(moduleId, BulkModuleUpdateAction.UNPUBLISH, false))
+ }
+ }
+
+ override fun updateFileModuleItem(fileId: Long, contentDetails: ModuleContentDetails) {
+ consumer?.accept(
+ ModuleListEvent.UpdateFileModuleItem(
+ fileId,
+ contentDetails
+ )
+ )
+ }
+
+ override fun showSnackbar(@StringRes message: Int, params: Array) {
+ consumer?.accept(ModuleListEvent.ShowSnackbar(message, params))
+ }
+
+ override fun updateModuleItem(itemId: Long, isPublished: Boolean) {
+ val title = if (isPublished) R.string.publishDialogTitle else R.string.unpublishDialogTitle
+ val message =
+ if (isPublished) R.string.publishModuleItemDialogMessage else R.string.unpublishModuleItemDialogMessage
+ val positiveButton = if (isPublished) R.string.publishDialogPositiveButton else R.string.unpublishDialogPositiveButton
+
+ showConfirmationDialog(title, message, positiveButton, R.string.cancel) {
+ consumer?.accept(ModuleListEvent.UpdateModuleItem(itemId, isPublished))
+ }
+ }
})
init {
@@ -67,6 +137,58 @@ class ModuleListView(
subtitle = course.name
setupBackButton(activity)
ViewStyler.themeToolbarColored(activity, this, course)
+ inflateMenu(R.menu.menu_module_list)
+ setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.actionPublishModulesItems -> {
+ showConfirmationDialog(
+ R.string.publishDialogTitle,
+ R.string.publishModulesAndItemsDialogMessage,
+ R.string.publishDialogPositiveButton,
+ R.string.cancel
+ ) {
+ consumer?.accept(
+ ModuleListEvent.BulkUpdateAllModules(
+ BulkModuleUpdateAction.PUBLISH,
+ false
+ )
+ )
+ }
+ true
+ }
+
+ R.id.actionPublishModules -> {
+ showConfirmationDialog(
+ R.string.publishDialogTitle,
+ R.string.publishModulesDialogMessage,
+ R.string.publishDialogPositiveButton,
+ R.string.cancel
+ ) {
+ consumer?.accept(ModuleListEvent.BulkUpdateAllModules(BulkModuleUpdateAction.PUBLISH, true))
+ }
+ true
+ }
+
+ R.id.actionUnpublishModulesItems -> {
+ showConfirmationDialog(
+ R.string.unpublishDialogTitle,
+ R.string.unpublishModulesAndItemsDialogMessage,
+ R.string.unpublishDialogPositiveButton,
+ R.string.cancel
+ ) {
+ consumer?.accept(
+ ModuleListEvent.BulkUpdateAllModules(
+ BulkModuleUpdateAction.UNPUBLISH,
+ false
+ )
+ )
+ }
+ true
+ }
+
+ else -> false
+ }
+ }
}
binding.recyclerView.apply {
@@ -103,4 +225,50 @@ class ModuleListView(
val itemPosition = adapter.getItemVisualPosition(itemId)
binding.recyclerView.scrollToPosition(itemPosition)
}
+
+ fun showConfirmationDialog(
+ title: Int,
+ message: Int,
+ positiveButton: Int,
+ negativeButton: Int,
+ onConfirmed: () -> Unit
+ ) {
+ AlertDialog.Builder(context)
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(positiveButton) { _, _ ->
+ onConfirmed()
+ }
+ .setNegativeButton(negativeButton) { _, _ -> }
+ .showThemed()
+ }
+
+ fun showSnackbar(@StringRes message: Int, params: Array = emptyArray()) {
+ Snackbar.make(binding.root, context.getString(message, *params), Snackbar.LENGTH_SHORT).show()
+ }
+
+ fun showUpdateFileDialog(fileId: Long, contentDetails: ModuleContentDetails) {
+ val fragment = UpdateFileDialogFragment.newInstance(fileId, contentDetails, course)
+ fragment.show((activity as FragmentActivity).supportFragmentManager, "editFileDialog")
+ }
+
+ fun showProgressDialog(
+ progressId: Long,
+ @StringRes title: Int,
+ @StringRes progressTitle: Int,
+ @StringRes note: Int? = null
+ ) {
+ val fragment = ProgressDialogFragment.newInstance(
+ progressId,
+ context.getString(title),
+ context.getString(progressTitle),
+ note?.let { context.getString(it) })
+ fragment.show((activity as FragmentActivity).supportFragmentManager, "progressDialog")
+ }
+
+ fun bulkUpdateInProgress(progresses: List) {
+ progresses.forEach {
+ consumer?.accept(ModuleListEvent.BulkUpdateStarted(course, it.progressId, it.allModules, it.skipContentTags, it.affectedIds, BulkModuleUpdateAction.valueOf(it.action)))
+ }
+ }
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListViewState.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListViewState.kt
index 5b000ef475..d38ce3ded6 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListViewState.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListViewState.kt
@@ -17,6 +17,8 @@
package com.instructure.teacher.features.modules.list.ui
import androidx.annotation.ColorInt
+import com.instructure.canvasapi2.models.ModuleContentDetails
+import com.instructure.canvasapi2.models.ModuleItem
data class ModuleListViewState(
val showRefreshing: Boolean = false,
@@ -36,11 +38,21 @@ sealed class ModuleListItemData {
data class InlineError(val buttonColor: Int): ModuleListItemData()
+ data class SubHeader(
+ val id: Long,
+ val title: String?,
+ val indent: Int,
+ val enabled: Boolean,
+ val published: Boolean?,
+ val isLoading: Boolean
+ ) : ModuleListItemData()
+
data class ModuleData(
val id: Long,
val name: String,
val isPublished: Boolean?,
- val moduleItems: List
+ val moduleItems: List,
+ val isLoading: Boolean
): ModuleListItemData()
data class ModuleItemData(
@@ -53,6 +65,9 @@ sealed class ModuleListItemData {
/** The subtitle. If null, the subtitle should be hidden. */
val subtitle: String?,
+ /** The second line of subtitle. If null, it should be hidden. */
+ val subtitle2: String?,
+
/** The resource ID of the icon to show for this item. If null, the icon should be hidden. */
val iconResId: Int?,
@@ -73,7 +88,15 @@ sealed class ModuleListItemData {
* Whether additional data is being loaded for this item, either for the purpose of routing or for the purpose
* of refreshing this item after it has been updated elsewhere in the app.
*/
- val isLoading: Boolean = false
+ val isLoading: Boolean = false,
+
+ val type: ModuleItem.Type,
+
+ val contentDetails: ModuleContentDetails? = null,
+
+ val contentId: Long? = null,
+
+ val unpublishable: Boolean = true
) : ModuleListItemData()
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/binders/ModuleListItemBinder.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/binders/ModuleListItemBinder.kt
index 7f6ef0fc00..754a9a8b8c 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/binders/ModuleListItemBinder.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/binders/ModuleListItemBinder.kt
@@ -16,7 +16,17 @@
*/
package com.instructure.teacher.features.modules.list.ui.binders
-import android.content.res.ColorStateList
+import android.view.Gravity
+import android.view.View
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.appcompat.widget.PopupMenu
+import com.instructure.canvasapi2.models.ModuleContentDetails
+import com.instructure.canvasapi2.models.ModuleItem
+import com.instructure.canvasapi2.utils.isValid
+import com.instructure.pandautils.binding.setTint
+import com.instructure.pandautils.utils.onClickWithRequireNetwork
import com.instructure.pandautils.utils.setTextForVisibility
import com.instructure.pandautils.utils.setVisible
import com.instructure.teacher.R
@@ -25,7 +35,8 @@ import com.instructure.teacher.databinding.AdapterModuleItemBinding
import com.instructure.teacher.features.modules.list.ui.ModuleListCallback
import com.instructure.teacher.features.modules.list.ui.ModuleListItemData
-class ModuleListItemBinder : ListItemBinder() {
+class ModuleListItemBinder :
+ ListItemBinder() {
override val layoutResId = R.layout.adapter_module_item
@@ -37,16 +48,125 @@ class ModuleListItemBinder : ListItemBinder {
+ if (data.contentDetails?.hidden == true) {
+ icon = R.drawable.ic_eye_off
+ tint = R.color.textWarning
+ contentDescription = R.string.a11y_hidden
+ } else if (data.contentDetails?.lockAt.isValid() || data.contentDetails?.unlockAt.isValid()) {
+ icon = R.drawable.ic_calendar_month
+ tint = R.color.textWarning
+ contentDescription = R.string.a11y_scheduled
+ } else {
+ icon =
+ if (data.isPublished == true) R.drawable.ic_complete_solid else R.drawable.ic_no
+ tint = if (data.isPublished == true) R.color.textSuccess else R.color.textDark
+ contentDescription =
+ if (data.isPublished == true) R.string.a11y_published else R.string.a11y_unpublished
+ }
+ }
+
+ else -> {
+ icon =
+ if (data.isPublished == true) R.drawable.ic_complete_solid else R.drawable.ic_no
+ tint = if (data.isPublished == true) R.color.textSuccess else R.color.textDark
+ contentDescription =
+ if (data.isPublished == true) R.string.a11y_published else R.string.a11y_unpublished
+ }
+ }
+
+ return StatusIcon(icon, tint, contentDescription)
+ }
+
+ private fun showModuleItemActions(
+ view: View,
+ item: ModuleListItemData.ModuleItemData,
+ callback: ModuleListCallback
+ ) {
+ val popup = PopupMenu(view.context, view, Gravity.START.and(Gravity.TOP))
+ val menu = popup.menu
+
+ when (item.isPublished) {
+ true -> menu.add(0, 0, 0, R.string.unpublishModuleItemAction)
+ false -> menu.add(0, 1, 1, R.string.publishModuleItemAction)
+ else -> {
+ menu.add(0, 0, 0, R.string.unpublishModuleItemAction)
+ menu.add(0, 1, 1, R.string.publishModuleItemAction)
+ }
}
+
+ popup.setOnMenuItemClickListener { menuItem ->
+ when (menuItem.itemId) {
+ 0 -> {
+ callback.updateModuleItem(item.id, false)
+ true
+ }
+
+ 1 -> {
+ callback.updateModuleItem(item.id, true)
+ true
+ }
+
+ else -> false
+ }
+ }
+
+ view.contentDescription =
+ view.context.getString(R.string.a11y_contentDescription_moduleOptions, item.title)
+ popup.show()
}
}
+
+data class StatusIcon(
+ @DrawableRes val icon: Int,
+ @ColorRes val tint: Int,
+ @StringRes val contentDescription: Int
+)
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/binders/ModuleListModuleBinder.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/binders/ModuleListModuleBinder.kt
index 74c6233299..b55752d97e 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/binders/ModuleListModuleBinder.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/binders/ModuleListModuleBinder.kt
@@ -16,6 +16,9 @@
*/
package com.instructure.teacher.features.modules.list.ui.binders
+import android.view.Gravity
+import androidx.appcompat.widget.PopupMenu
+import com.instructure.pandautils.utils.onClickWithRequireNetwork
import com.instructure.pandautils.utils.setVisible
import com.instructure.teacher.R
import com.instructure.teacher.adapters.ListItemBinder
@@ -31,13 +34,41 @@ class ModuleListModuleBinder : ListItemBinder callback.markModuleExpanded(item.id, isExpanded) },
- onBind = { item, view, isCollapsed, _ ->
+ onBind = { item, view, isCollapsed, callback ->
val binding = AdapterModuleBinding.bind(view)
with(binding) {
moduleName.text = item.name
- publishedIcon.setVisible(item.isPublished == true)
- unpublishedIcon.setVisible(item.isPublished == false)
- collapseIcon.rotation = if (isCollapsed) 0f else 180f
+ publishedIcon.setVisible(item.isPublished == true && !item.isLoading)
+ unpublishedIcon.setVisible(item.isPublished == false && !item.isLoading)
+ collapseIcon.rotation = if (isCollapsed) 180f else 0f
+
+ loadingView.setVisible(item.isLoading)
+
+ publishActions.onClickWithRequireNetwork {
+ val popup = PopupMenu(it.context, it, Gravity.START.and(Gravity.TOP))
+ popup.inflate(R.menu.menu_module)
+
+ popup.setOnMenuItemClickListener { menuItem ->
+ when (menuItem.itemId) {
+ R.id.publishModuleItems -> {
+ callback.publishModuleAndItems(item.id)
+ true
+ }
+ R.id.publishModule -> {
+ callback.publishModule(item.id)
+ true
+ }
+ R.id.unpublishModuleItems -> {
+ callback.unpublishModuleAndItems(item.id)
+ true
+ }
+ else -> false
+ }
+ }
+
+ publishActions.contentDescription = it.context.getString(R.string.a11y_contentDescription_moduleOptions, item.name)
+ popup.show()
+ }
}
}
)
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/binders/ModuleListSubHeaderBinder.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/binders/ModuleListSubHeaderBinder.kt
new file mode 100644
index 0000000000..de662ee022
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/binders/ModuleListSubHeaderBinder.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.instructure.teacher.features.modules.list.ui.binders
+
+import android.view.Gravity
+import androidx.appcompat.widget.PopupMenu
+import com.instructure.pandautils.utils.onClickWithRequireNetwork
+import com.instructure.pandautils.utils.setHidden
+import com.instructure.pandautils.utils.setVisible
+import com.instructure.teacher.R
+import com.instructure.teacher.adapters.ListItemBinder
+import com.instructure.teacher.databinding.AdapterModuleSubHeaderBinding
+import com.instructure.teacher.features.modules.list.ui.ModuleListCallback
+import com.instructure.teacher.features.modules.list.ui.ModuleListItemData
+
+class ModuleListSubHeaderBinder : ListItemBinder() {
+ override val layoutResId = R.layout.adapter_module_sub_header
+
+ override fun getItemId(item: ModuleListItemData.SubHeader) = item.id
+
+ override val bindBehavior: BindBehavior = Item { item, view, callback ->
+ val binding = AdapterModuleSubHeaderBinding.bind(view)
+ with(binding) {
+ subHeaderTitle.text = item.title
+ moduleItemPublishedIcon.setVisible(item.published == true && !item.isLoading)
+ moduleItemUnpublishedIcon.setVisible(item.published == false && !item.isLoading)
+ moduleItemIndent.layoutParams.width = item.indent
+
+ moduleItemLoadingView.setVisible(item.isLoading)
+
+ publishActions.onClickWithRequireNetwork {
+ val popup = PopupMenu(it.context, it, Gravity.START.and(Gravity.TOP))
+ val menu = popup.menu
+
+ when (item.published) {
+ true -> menu.add(0, 0, 0, R.string.unpublish)
+ false -> menu.add(0, 1, 1, R.string.publish)
+ else -> {
+ menu.add(0, 0, 0, R.string.unpublish)
+ menu.add(0, 1, 1, R.string.publish)
+ }
+ }
+
+ popup.setOnMenuItemClickListener { menuItem ->
+ when (menuItem.itemId) {
+ 0 -> {
+ callback.updateModuleItem(item.id, false)
+ true
+ }
+
+ 1 -> {
+ callback.updateModuleItem(item.id, true)
+ true
+ }
+
+ else -> false
+ }
+ }
+
+ publishActions.contentDescription = it.context.getString(R.string.a11y_contentDescription_moduleOptions, item.title)
+ popup.show()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/file/UpdateFileDialogFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/file/UpdateFileDialogFragment.kt
new file mode 100644
index 0000000000..0ec264a5ea
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/file/UpdateFileDialogFragment.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.instructure.teacher.features.modules.list.ui.file
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CompoundButton
+import androidx.core.widget.CompoundButtonCompat
+import androidx.fragment.app.viewModels
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.ModuleContentDetails
+import com.instructure.pandautils.dialogs.DatePickerDialogFragment
+import com.instructure.pandautils.dialogs.TimePickerDialogFragment
+import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.ParcelableArg
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.pandautils.utils.children
+import com.instructure.pandautils.utils.textAndIconColor
+import com.instructure.teacher.databinding.FragmentDialogUpdateFileBinding
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class UpdateFileDialogFragment : BottomSheetDialogFragment() {
+
+ private val canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT)
+
+ private lateinit var binding: FragmentDialogUpdateFileBinding
+
+ private val viewModel: UpdateFileViewModel by viewModels()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ binding = FragmentDialogUpdateFileBinding.inflate(inflater, container, false)
+ binding.viewModel = viewModel
+ binding.lifecycleOwner = this
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel.events.observe(viewLifecycleOwner) {
+ it.getContentIfNotHandled()?.let {
+ handleAction(it)
+ }
+ }
+
+ setRadioButtonColors()
+
+ binding.updateButton.setTextColor(canvasContext.textAndIconColor)
+ }
+
+ private fun setRadioButtonColors() = with(binding) {
+ val radioButtonColor = ViewStyler.makeColorStateListForRadioGroup(
+ requireContext().getColor(com.instructure.pandautils.R.color.textDarkest), canvasContext.textAndIconColor
+ )
+
+ val radioButtons =
+ availabilityRadioGroup.children.filterIsInstance() + visibilityRadioGroup.children.filterIsInstance()
+
+ radioButtons.forEach {
+ CompoundButtonCompat.setButtonTintList(it, radioButtonColor)
+ }
+ }
+
+ private fun handleAction(event: UpdateFileEvent) {
+ when (event) {
+ is UpdateFileEvent.Close -> dismiss()
+ is UpdateFileEvent.ShowDatePicker -> {
+ val dialog = DatePickerDialogFragment.getInstance(
+ manager = childFragmentManager,
+ defaultDate = event.selectedDate,
+ minDate = event.minDate,
+ maxDate = event.maxDate,
+ callback = event.callback
+ )
+ dialog.show(childFragmentManager, "datePicker")
+ }
+
+ is UpdateFileEvent.ShowTimePicker -> {
+ val dialog = TimePickerDialogFragment.getInstance(
+ childFragmentManager,
+ event.selectedDate,
+ event.callback
+ )
+ dialog.show(childFragmentManager, "timePicker")
+ }
+ }
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return super.onCreateDialog(savedInstanceState).apply {
+ setOnShowListener {
+ val bottomSheet = findViewById(com.google.android.material.R.id.design_bottom_sheet)
+ val behavior = BottomSheetBehavior.from(bottomSheet)
+ bottomSheet.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+ behavior.skipCollapsed = true
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ behavior.peekHeight = 0
+ bottomSheet.parent.requestLayout()
+ }
+ }
+ }
+
+ companion object {
+
+ fun newInstance(
+ contentId: Long,
+ contentDetails: ModuleContentDetails?,
+ canvasContext: CanvasContext
+ ): UpdateFileDialogFragment {
+ return UpdateFileDialogFragment().apply {
+ arguments = Bundle().apply {
+ putLong("contentId", contentId)
+ putParcelable("contentDetails", contentDetails)
+ putParcelable(Const.CANVAS_CONTEXT, canvasContext)
+ }
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/file/UpdateFileViewData.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/file/UpdateFileViewData.kt
new file mode 100644
index 0000000000..0caa52feee
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/file/UpdateFileViewData.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.instructure.teacher.features.modules.list.ui.file
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import com.instructure.teacher.R
+import java.util.Date
+
+data class UpdateFileViewData(
+ val selectedAvailability: FileAvailability,
+ val selectedVisibility: FileVisibility,
+ val lockAt: Date?,
+ val unlockAt: Date?,
+ val lockAtDateString: String?,
+ val lockAtTimeString: String?,
+ val unlockAtDateString: String?,
+ val unlockAtTimeString: String?
+)
+
+enum class FileVisibility {
+ INHERIT,
+ CONTEXT,
+ INSTITUTION,
+ PUBLIC
+}
+
+enum class FileAvailability {
+ PUBLISHED,
+ UNPUBLISHED,
+ HIDDEN,
+ SCHEDULED
+}
+
+sealed class UpdateFileEvent {
+ object Close : UpdateFileEvent()
+
+ data class ShowDatePicker(
+ val selectedDate: Date?,
+ val minDate: Date? = null,
+ val maxDate: Date? = null,
+ val callback: (year: Int, month: Int, dayOfMonth: Int) -> Unit
+ ) : UpdateFileEvent()
+
+ data class ShowTimePicker(
+ val selectedDate: Date?,
+ val callback: (hourOfDay: Int, minute: Int) -> Unit
+ ) : UpdateFileEvent()
+}
\ No newline at end of file
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/file/UpdateFileViewModel.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/file/UpdateFileViewModel.kt
new file mode 100644
index 0000000000..454a20bc54
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/file/UpdateFileViewModel.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.instructure.teacher.features.modules.list.ui.file
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.instructure.canvasapi2.apis.FileFolderAPI
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.ModuleContentDetails
+import com.instructure.canvasapi2.models.UpdateFileFolder
+import com.instructure.canvasapi2.utils.DateHelper
+import com.instructure.canvasapi2.utils.toApiString
+import com.instructure.pandautils.mvvm.Event
+import com.instructure.pandautils.mvvm.ViewState
+import com.instructure.pandautils.utils.FileFolderUpdatedEvent
+import com.instructure.teacher.R
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.launch
+import org.greenrobot.eventbus.EventBus
+import java.util.Calendar
+import java.util.Date
+import javax.inject.Inject
+
+@HiltViewModel
+@SuppressLint("StaticFieldLeak")
+class UpdateFileViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ @ApplicationContext private val context: Context,
+ private val fileApi: FileFolderAPI.FilesFoldersInterface,
+ private val eventBus: EventBus
+) : ViewModel() {
+
+ private val fileId: Long = savedStateHandle.get("contentId") ?: -1L
+ private val contentDetails: ModuleContentDetails = savedStateHandle.get("contentDetails") ?: ModuleContentDetails()
+
+ val data: LiveData
+ get() = _data
+ private val _data = MutableLiveData()
+
+ val events: LiveData>
+ get() = _events
+ private val _events = MutableLiveData>()
+
+ val state: LiveData
+ get() = _state
+ private val _state = MutableLiveData()
+
+ init {
+ _state.postValue(ViewState.Loading)
+ loadData()
+ }
+
+ private fun loadData() {
+ viewModelScope.launch {
+ val file = fileApi.getFile(fileId, RestParams(isForceReadFromNetwork = true)).dataOrNull
+
+ val availability = when {
+ contentDetails.locked == true -> FileAvailability.UNPUBLISHED
+ contentDetails.hidden == true -> FileAvailability.HIDDEN
+ contentDetails.lockAt != null || contentDetails.unlockAt != null -> FileAvailability.SCHEDULED
+ else -> FileAvailability.PUBLISHED
+ }
+
+ val visibility =
+ file?.visibilityLevel?.let { FileVisibility.valueOf(it.uppercase()) } ?: FileVisibility.INHERIT
+
+ _data.postValue(
+ UpdateFileViewData(
+ selectedAvailability = availability,
+ selectedVisibility = visibility,
+ lockAt = contentDetails.lockDate,
+ unlockAt = contentDetails.unlockDate,
+ lockAtDateString = DateHelper.getFormattedDate(context, contentDetails.lockDate),
+ lockAtTimeString = DateHelper.getFormattedTime(context, contentDetails.lockDate),
+ unlockAtDateString = DateHelper.getFormattedDate(context, contentDetails.unlockDate),
+ unlockAtTimeString = DateHelper.getFormattedTime(context, contentDetails.unlockDate),
+ )
+ )
+ _state.postValue(ViewState.Success)
+ }
+ }
+
+ fun onAvailabilityChanged(availability: FileAvailability) {
+ _data.postValue(
+ data.value?.copy(
+ selectedAvailability = availability
+ )
+ )
+ }
+
+ fun onVisibilityChanged(visibility: FileVisibility) {
+ _data.postValue(
+ data.value?.copy(
+ selectedVisibility = visibility
+ )
+ )
+ }
+
+ fun close() {
+ _events.postValue(Event(UpdateFileEvent.Close))
+ }
+
+ fun update() {
+ viewModelScope.launch {
+ _state.postValue(ViewState.Loading)
+ val updateFileFolder = UpdateFileFolder(
+ locked = data.value?.selectedAvailability == FileAvailability.UNPUBLISHED,
+ hidden = data.value?.selectedAvailability == FileAvailability.HIDDEN,
+ lockAt = if (data.value?.selectedAvailability == FileAvailability.SCHEDULED) data.value?.lockAt?.toApiString()
+ .orEmpty() else "",
+ unlockAt = if (data.value?.selectedAvailability == FileAvailability.SCHEDULED) data.value?.unlockAt?.toApiString()
+ .orEmpty() else "",
+ visibilityLevel = data.value?.selectedVisibility?.name?.lowercase()
+ )
+ val updatedFile =
+ fileApi.updateFile(fileId, updateFileFolder, RestParams(isForceReadFromNetwork = true)).dataOrNull
+ if (updatedFile != null) {
+ eventBus.post(FileFolderUpdatedEvent(updatedFile))
+ _events.postValue(Event(UpdateFileEvent.Close))
+ } else {
+ _state.postValue(
+ ViewState.Error(
+ context.getString(R.string.errorOccurred),
+ R.drawable.ic_panda_nofiles
+ )
+ )
+ }
+ }
+ }
+
+ fun updateLockAt() {
+ _events.postValue(Event(UpdateFileEvent.ShowDatePicker(selectedDate = data.value?.lockAt, minDate = data.value?.unlockAt, callback = ::setLockAtDate)))
+ }
+
+ fun updateLockTime() {
+ _events.postValue(Event(UpdateFileEvent.ShowTimePicker(data.value?.lockAt, ::setLockTime)))
+ }
+
+ fun updateUnlockAt() {
+ _events.postValue(Event(UpdateFileEvent.ShowDatePicker(selectedDate = data.value?.unlockAt, maxDate = data.value?.lockAt, callback = ::setUnlockAtDate)))
+ }
+
+ fun updateUnlockTime() {
+ _events.postValue(Event(UpdateFileEvent.ShowTimePicker(data.value?.unlockAt, ::setUnlockTime)))
+ }
+
+ private fun setUnlockAtDate(year: Int, month: Int, day: Int) {
+ val selectedDate = getSelectedDate(data.value?.unlockAt, year, month, day)
+ _data.postValue(
+ data.value?.copy(
+ unlockAt = selectedDate,
+ unlockAtDateString = DateHelper.getFormattedDate(context, selectedDate),
+ unlockAtTimeString = DateHelper.getFormattedTime(context, selectedDate)
+ )
+ )
+ }
+
+ private fun setLockAtDate(year: Int, month: Int, day: Int) {
+ val selectedDate = getSelectedDate(data.value?.lockAt, year, month, day)
+ _data.postValue(
+ data.value?.copy(
+ lockAt = selectedDate,
+ lockAtDateString = DateHelper.getFormattedDate(context, selectedDate),
+ lockAtTimeString = DateHelper.getFormattedTime(context, selectedDate)
+ )
+ )
+ }
+
+ private fun setUnlockTime(hourOfDay: Int, minutes: Int) {
+ val selectedDate = getSelectedTime(data.value?.unlockAt, hourOfDay, minutes)
+ _data.postValue(
+ data.value?.copy(
+ unlockAt = selectedDate,
+ unlockAtDateString = DateHelper.getFormattedDate(context, selectedDate),
+ unlockAtTimeString = DateHelper.getFormattedTime(context, selectedDate)
+ )
+ )
+ }
+
+ private fun setLockTime(hourOfDay: Int, minutes: Int) {
+ val selectedDate = getSelectedTime(data.value?.lockAt, hourOfDay, minutes)
+ _data.postValue(
+ data.value?.copy(
+ lockAt = selectedDate,
+ lockAtDateString = DateHelper.getFormattedDate(context, selectedDate),
+ lockAtTimeString = DateHelper.getFormattedTime(context, selectedDate)
+ )
+ )
+ }
+
+ fun clearUnlockDate() {
+ _data.postValue(
+ data.value?.copy(
+ unlockAt = null,
+ unlockAtDateString = null,
+ unlockAtTimeString = null
+ )
+ )
+ }
+
+ fun clearLockDate() {
+ _data.postValue(
+ data.value?.copy(
+ lockAt = null,
+ lockAtDateString = null,
+ lockAtTimeString = null
+ )
+ )
+ }
+
+ private fun getSelectedDate(previousDate: Date?, year: Int, month: Int, day: Int): Date {
+ val calendar = Calendar.getInstance()
+ calendar.time = previousDate ?: Date()
+ calendar.set(year, month, day, 0, 0)
+ return calendar.time
+ }
+
+ private fun getSelectedTime(previousDate: Date?, hourOfDay: Int, minutes: Int): Date {
+ val calendar = Calendar.getInstance()
+ calendar.time = previousDate ?: Date()
+ calendar.set(Calendar.HOUR_OF_DAY, hourOfDay)
+ calendar.set(Calendar.MINUTE, minutes)
+ return calendar.time
+ }
+}
\ No newline at end of file
diff --git a/apps/teacher/src/main/res/layout/activity_route_validator.xml b/apps/teacher/src/main/res/layout/activity_route_validator.xml
index ac79f0f7b2..d5922f088f 100644
--- a/apps/teacher/src/main/res/layout/activity_route_validator.xml
+++ b/apps/teacher/src/main/res/layout/activity_route_validator.xml
@@ -23,6 +23,12 @@
android:background="@color/backgroundLightest"
android:orientation="vertical">
+
+
-
+ android:orientation="vertical">
-
+ android:background="@color/backgroundLight"
+ android:foreground="?attr/selectableItemBackground"
+ android:gravity="center_vertical"
+ android:minHeight="48dp"
+ android:orientation="horizontal"
+ android:paddingStart="8dp"
+ android:paddingTop="8dp"
+ android:paddingEnd="4dp"
+ android:paddingBottom="8dp">
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
+
+
diff --git a/apps/teacher/src/main/res/layout/adapter_module_item.xml b/apps/teacher/src/main/res/layout/adapter_module_item.xml
index d4ffe7c6bd..1ec6800135 100644
--- a/apps/teacher/src/main/res/layout/adapter_module_item.xml
+++ b/apps/teacher/src/main/res/layout/adapter_module_item.xml
@@ -1,5 +1,4 @@
-
-
-
+ android:paddingTop="12dp"
+ android:paddingEnd="4dp"
+ android:paddingBottom="12dp">
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+ android:tint="@color/textDark"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@id/moduleItemIndent"
+ app:layout_constraintTop_toTopOf="parent" />
-
+
-
+ android:layout_marginHorizontal="16dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/textDark"
+ app:layout_constraintBottom_toTopOf="@+id/moduleItemSubtitle2"
+ app:layout_constraintEnd_toStartOf="@+id/publishActions"
+ app:layout_constraintStart_toEndOf="@id/moduleItemIcon"
+ app:layout_constraintTop_toBottomOf="@id/moduleItemTitle"
+ tools:text="Due Apr 25 at 11:59pm" />
+
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/apps/teacher/src/main/res/layout/adapter_module_sub_header.xml b/apps/teacher/src/main/res/layout/adapter_module_sub_header.xml
new file mode 100644
index 0000000000..c862cae2c0
--- /dev/null
+++ b/apps/teacher/src/main/res/layout/adapter_module_sub_header.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/teacher/src/main/res/layout/fragment_dialog_update_file.xml b/apps/teacher/src/main/res/layout/fragment_dialog_update_file.xml
new file mode 100644
index 0000000000..ab99ba3b71
--- /dev/null
+++ b/apps/teacher/src/main/res/layout/fragment_dialog_update_file.xml
@@ -0,0 +1,444 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/teacher/src/main/res/menu/menu_module.xml b/apps/teacher/src/main/res/menu/menu_module.xml
new file mode 100644
index 0000000000..6f8b60a129
--- /dev/null
+++ b/apps/teacher/src/main/res/menu/menu_module.xml
@@ -0,0 +1,39 @@
+
+
+
\ No newline at end of file
diff --git a/apps/teacher/src/main/res/menu/menu_module_list.xml b/apps/teacher/src/main/res/menu/menu_module_list.xml
new file mode 100644
index 0000000000..a5d56562b9
--- /dev/null
+++ b/apps/teacher/src/main/res/menu/menu_module_list.xml
@@ -0,0 +1,39 @@
+
+
+
\ No newline at end of file
diff --git a/apps/teacher/src/main/res/values-ja/strings.xml b/apps/teacher/src/main/res/values-ja/strings.xml
index 15921efd04..65996d9873 100644
--- a/apps/teacher/src/main/res/values-ja/strings.xml
+++ b/apps/teacher/src/main/res/values-ja/strings.xml
@@ -187,7 +187,7 @@
グループ内にユーザーなし
公開済み
- 未公開
+ 非公開
公開されたチェックマーク
ポイント
ポイント
@@ -677,7 +677,7 @@
新しいディスカッション
オプション
- 定期購読
+ 常に通知
スレッドでの返信を許可する
ユーザは返信を表示する前に投稿しなければなりません
ユーザーにコメントを許可する
@@ -806,7 +806,7 @@
ファイルを作成する
ファイルの作成オタンとフォルダの作成ボタンを表示する
ファイルの作成とフォルダの作成ボタンを非表示にする
- 未公開
+ 非公開
制限されたアクセス
部外秘
非表示の内部ファイルはリンクで利用できます。
@@ -826,7 +826,7 @@
ページを削除
これにより、ページは削除されます。この操作は元に戻すことはできません。
この発表を保存しようとしてエラーが発生しました。もう一度試してください。
- ページを先フロントページにした場合、未公開にすることはできません。
+ ページを先フロントページにした場合、非公開にすることはできません。
教員のみ
教員と受講者
diff --git a/apps/teacher/src/main/res/values/strings.xml b/apps/teacher/src/main/res/values/strings.xml
index 15abd0fa7c..964cd80ae9 100644
--- a/apps/teacher/src/main/res/values/strings.xml
+++ b/apps/teacher/src/main/res/values/strings.xml
@@ -898,5 +898,6 @@
No grade
Excuse
Overgraded by %s
+ Cannot unpublish %s if there are student submissions
diff --git a/apps/teacher/src/qa/java/com/instructure/teacher/SingleFragmentTestActivity.kt b/apps/teacher/src/qa/java/com/instructure/teacher/SingleFragmentTestActivity.kt
index be89c34695..50125e5773 100644
--- a/apps/teacher/src/qa/java/com/instructure/teacher/SingleFragmentTestActivity.kt
+++ b/apps/teacher/src/qa/java/com/instructure/teacher/SingleFragmentTestActivity.kt
@@ -23,7 +23,9 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import com.instructure.pandautils.binding.viewBinding
import com.instructure.teacher.databinding.ActivitySingleFragmentTestBinding
+import dagger.hilt.android.AndroidEntryPoint
+@AndroidEntryPoint
class SingleFragmentTestActivity : AppCompatActivity() {
private val binding by viewBinding(ActivitySingleFragmentTestBinding::inflate)
diff --git a/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListEffectHandlerTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListEffectHandlerTest.kt
index fc93c7acc4..137f52ebe2 100644
--- a/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListEffectHandlerTest.kt
+++ b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListEffectHandlerTest.kt
@@ -15,15 +15,24 @@
*/
package com.instructure.teacher.unit.modules.list
+import com.instructure.canvasapi2.apis.ModuleAPI
+import com.instructure.canvasapi2.apis.ProgressAPI
import com.instructure.canvasapi2.managers.ModuleManager
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.ModuleContentDetails
import com.instructure.canvasapi2.models.ModuleItem
import com.instructure.canvasapi2.models.ModuleObject
+import com.instructure.canvasapi2.models.Progress
+import com.instructure.canvasapi2.models.postmodels.BulkUpdateProgress
+import com.instructure.canvasapi2.models.postmodels.BulkUpdateResponse
import com.instructure.canvasapi2.utils.DataResult
import com.instructure.canvasapi2.utils.Failure
import com.instructure.canvasapi2.utils.weave.awaitApi
import com.instructure.canvasapi2.utils.weave.awaitApiResponse
+import com.instructure.pandautils.features.progress.ProgressPreferences
+import com.instructure.pandautils.room.appdatabase.daos.ModuleBulkProgressDao
+import com.instructure.teacher.features.modules.list.BulkModuleUpdateAction
import com.instructure.teacher.features.modules.list.CollapsedModulesStore
import com.instructure.teacher.features.modules.list.ModuleListEffect
import com.instructure.teacher.features.modules.list.ModuleListEffectHandler
@@ -33,6 +42,7 @@ import com.instructure.teacher.features.modules.list.ui.ModuleListView
import com.spotify.mobius.Connection
import com.spotify.mobius.functions.Consumer
import io.mockk.Ordering
+import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.confirmVerified
import io.mockk.every
@@ -48,6 +58,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.test.setMain
import okhttp3.Headers.Companion.toHeaders
+import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
@@ -62,14 +73,31 @@ class ModuleListEffectHandlerTest : Assert() {
private lateinit var connection: Connection
private val course: CanvasContext = Course()
+ private val moduleApi: ModuleAPI.ModuleInterface = mockk(relaxed = true)
+ private val progressApi: ProgressAPI.ProgressInterface = mockk(relaxed = true)
+ private val progressPreferences: ProgressPreferences = mockk(relaxed = true)
+ private val moduleBulkProgressDao: ModuleBulkProgressDao = mockk(relaxed = true)
+
@ExperimentalCoroutinesApi
@Before
fun setUp() {
Dispatchers.setMain(Executors.newSingleThreadExecutor().asCoroutineDispatcher())
view = mockk(relaxed = true)
- effectHandler = ModuleListEffectHandler().apply { view = this@ModuleListEffectHandlerTest.view }
+ effectHandler =
+ ModuleListEffectHandler(moduleApi, progressApi, progressPreferences, moduleBulkProgressDao).apply {
+ view = this@ModuleListEffectHandlerTest.view
+ }
consumer = mockk(relaxed = true)
connection = effectHandler.connect(consumer)
+
+ every { progressPreferences.cancelledProgressIds } returns mutableSetOf()
+ every { progressPreferences.cancelledProgressIds = any() } returns Unit
+ coEvery { moduleBulkProgressDao.findByCourseId(any()) } returns emptyList()
+ }
+
+ @After
+ fun teardown() {
+ clearAllMocks()
}
@Test
@@ -191,7 +219,8 @@ class ModuleListEffectHandlerTest : Assert() {
val nextUrl1 = "fake_next_url_1"
val nextUrl2 = "fake_next_url_2"
val firstPageModules = makeModulePage(pageNumber = 0)
- val secondPageModules = listOf(ModuleObject(id = moduleId, itemCount = 1, items = listOf(ModuleItem(scrollToItemId))))
+ val secondPageModules =
+ listOf(ModuleObject(id = moduleId, itemCount = 1, items = listOf(ModuleItem(scrollToItemId))))
val thirdPageModules = makeModulePage(pageNumber = 1)
val expectedEvent = ModuleListEvent.PageLoaded(
@@ -281,6 +310,249 @@ class ModuleListEffectHandlerTest : Assert() {
unmockkStatic("com.instructure.canvasapi2.utils.weave.AwaitApiKt")
}
+ @Test
+ fun `BulkUpdateStarted results in correct success event`() {
+ val pageModules = makeModulePage()
+ val expectedEvent = ModuleListEvent.BulkUpdateSuccess(false, BulkModuleUpdateAction.PUBLISH, false)
+
+ coEvery {
+ moduleApi.bulkUpdateModules(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any()
+ )
+ } returns DataResult.Success(mockk(relaxed = true))
+ coEvery { progressApi.getProgress(any(), any()) } returns DataResult.Success(
+ Progress(
+ 1L,
+ workflowState = "completed"
+ )
+ )
+
+ connection.accept(
+ ModuleListEffect.BulkUpdateStarted(
+ 1L,
+ false,
+ false,
+ BulkModuleUpdateAction.PUBLISH
+ )
+ )
+
+ verify(timeout = 1000) { consumer.accept(expectedEvent) }
+ confirmVerified(consumer)
+ }
+
+ @Test
+ fun `BulkUpdateModules results in correct failed event when call fails`() {
+ val pageModules = makeModulePage()
+ val expectedEvent = ModuleListEvent.BulkUpdateFailed(false)
+
+ coEvery {
+ moduleApi.bulkUpdateModules(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any()
+ )
+ } returns DataResult.Fail()
+
+ connection.accept(
+ ModuleListEffect.BulkUpdateModules(
+ course,
+ pageModules.map { it.id },
+ pageModules.map { it.id } + pageModules.flatMap { it.items.map { it.id } },
+ BulkModuleUpdateAction.PUBLISH,
+ false,
+ false
+ )
+ )
+
+ verify(timeout = 1000) { consumer.accept(expectedEvent) }
+ confirmVerified(consumer)
+ }
+
+ @Test
+ fun `BulkUpdateStarted results in correct failed event when progress fails`() {
+ val pageModules = makeModulePage()
+ val expectedEvent = ModuleListEvent.BulkUpdateFailed(false)
+
+ coEvery {
+ moduleApi.bulkUpdateModules(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any()
+ )
+ } returns DataResult.Success(mockk(relaxed = true))
+ coEvery { progressApi.getProgress(any(), any()) } returns DataResult.Success(
+ Progress(
+ 1L,
+ workflowState = "failed"
+ )
+ )
+
+ connection.accept(
+ ModuleListEffect.BulkUpdateStarted(
+ 1L,
+ false,
+ false,
+ BulkModuleUpdateAction.PUBLISH
+ )
+ )
+
+ verify(timeout = 1000) { consumer.accept(expectedEvent) }
+ confirmVerified(consumer)
+ }
+
+ @Test
+ fun `UpdateModuleItem results in correct success event`() {
+ val moduleId = 1L
+ val itemId = 2L
+ val expectedEvent = ModuleListEvent.ModuleItemUpdateSuccess(
+ ModuleItem(id = itemId, moduleId = moduleId, published = true),
+ true
+ )
+
+ coEvery { moduleApi.publishModuleItem(any(), any(), any(), any(), any(), any()) } returns DataResult.Success(
+ ModuleItem(2L, 1L, published = true)
+ )
+
+ connection.accept(ModuleListEffect.UpdateModuleItem(course, moduleId, itemId, true))
+
+ verify(timeout = 100) { consumer.accept(expectedEvent) }
+ confirmVerified(consumer)
+ }
+
+ @Test
+ fun `UpdateModuleItem results in correct failed event`() {
+ val moduleId = 1L
+ val itemId = 2L
+ val expectedEvent = ModuleListEvent.ModuleItemUpdateFailed(itemId)
+
+ coEvery { moduleApi.publishModuleItem(any(), any(), any(), any(), any(), any()) } returns DataResult.Fail()
+
+ connection.accept(ModuleListEffect.UpdateModuleItem(course, moduleId, itemId, true))
+
+ verify(timeout = 100) { consumer.accept(expectedEvent) }
+ confirmVerified(consumer)
+ }
+
+ @Test
+ fun `ShowSnackbar calls showSnackbar on view`() {
+ val message = 123
+ connection.accept(ModuleListEffect.ShowSnackbar(message))
+ verify(timeout = 100) { view.showSnackbar(message) }
+ confirmVerified(view)
+ }
+
+ @Test
+ fun `UpdateFileModuleItem calls showUpdateFileDialog on view`() {
+ val fileId = 123L
+ val contentDetails = ModuleContentDetails()
+ connection.accept(ModuleListEffect.UpdateFileModuleItem(fileId, contentDetails))
+ verify(timeout = 100) { view.showUpdateFileDialog(fileId, contentDetails) }
+ confirmVerified(view)
+ }
+
+ @Test
+ fun `Bulk update cancel emits correct event`() {
+ val pageModules = makeModulePage()
+ val expectedEvent = ModuleListEvent.BulkUpdateCancelled
+
+ coEvery {
+ moduleApi.bulkUpdateModules(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any()
+ )
+ } returns DataResult.Success(
+ BulkUpdateResponse(BulkUpdateProgress(Progress(id = 1L)))
+ )
+ coEvery { progressApi.getProgress(any(), any()) } returns DataResult.Success(
+ Progress(
+ 1L,
+ workflowState = "failed"
+ )
+ )
+ every { progressPreferences.cancelledProgressIds } returns mutableSetOf(1L)
+
+ connection.accept(
+ ModuleListEffect.BulkUpdateStarted(
+ 1L,
+ false,
+ false,
+ BulkModuleUpdateAction.PUBLISH
+ )
+ )
+
+ verify(timeout = 1000) { consumer.accept(expectedEvent) }
+ confirmVerified(consumer)
+ }
+
+ @Test
+ fun `BulkUpdateModules result in correct event`() {
+ val pageModules = makeModulePage()
+ val expectedEvent = ModuleListEvent.BulkUpdateStarted(
+ course,
+ 0L,
+ true,
+ false,
+ pageModules.map { it.id } + pageModules.flatMap { it.items.map { it.id } },
+ BulkModuleUpdateAction.PUBLISH)
+
+ coEvery {
+ moduleApi.bulkUpdateModules(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any()
+ )
+ } returns DataResult.Success(mockk(relaxed = true))
+
+ connection.accept(
+ ModuleListEffect.BulkUpdateModules(
+ course,
+ pageModules.map { it.id },
+ pageModules.map { it.id } + pageModules.flatMap { it.items.map { it.id } },
+ BulkModuleUpdateAction.PUBLISH,
+ false,
+ true
+ )
+ )
+
+ verify(timeout = 1000) { consumer.accept(expectedEvent) }
+ confirmVerified(consumer)
+ }
+
+ @Test
+ fun `ShowSnackbar with params calls showSnackbar on view`() {
+ val message = 123
+ val params = arrayOf("param1", "param2")
+ connection.accept(ModuleListEffect.ShowSnackbar(message, params))
+ verify(timeout = 100) { view.showSnackbar(message, params) }
+ confirmVerified(view)
+ }
+
+ private fun makeLinkHeader(nextUrl: String) =
+ mapOf("Link" to """<$nextUrl>; rel="next"""").toHeaders()
+
private fun makeModulePage(
moduleCount: Int = 3,
itemsPerModule: Int = 3,
@@ -296,8 +568,4 @@ class ModuleListEffectHandlerTest : Assert() {
}
}
-
- private fun makeLinkHeader(nextUrl: String) =
- mapOf("Link" to """<$nextUrl>; rel="next"""").toHeaders()
-
}
diff --git a/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListPresenterTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListPresenterTest.kt
index a26de81beb..25ee3f90c1 100644
--- a/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListPresenterTest.kt
+++ b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListPresenterTest.kt
@@ -63,6 +63,7 @@ class ModuleListPresenterTest : Assert() {
id = 1L,
name = "Module 1",
isPublished = true,
+ isLoading = false,
moduleItems = listOf(ModuleListItemData.EmptyItem(1L))
)
moduleItemTemplate = ModuleItem(
@@ -78,12 +79,18 @@ class ModuleListPresenterTest : Assert() {
moduleItemDataTemplate = ModuleListItemData.ModuleItemData(
id = 1000L,
title = "Module Item 1",
- subtitle = "Due February 12, 2050 at 3:07 PM",
+ subtitle = "February 12, 2050 at 3:07 PM",
+ subtitle2 = null,
iconResId = R.drawable.ic_assignment,
isPublished = true,
indent = 0,
tintColor = course.backgroundColor,
- enabled = true
+ enabled = true,
+ type = ModuleItem.Type.Assignment,
+ contentDetails = ModuleContentDetails(
+ dueAt = DateHelper.makeDate(2050, 1, 12, 15, 7, 0).toApiString()
+ ),
+ contentId = 0
)
modelTemplate = ModuleListModel(
course = course,
@@ -210,7 +217,8 @@ class ModuleListPresenterTest : Assert() {
)
val expectedState = moduleItemDataTemplate.copy(
title = item.title,
- iconResId = R.drawable.ic_discussion
+ iconResId = R.drawable.ic_discussion,
+ type = ModuleItem.Type.Discussion
)
val viewState = ModuleListPresenter.present(model, context)
val itemState = (viewState.items[0] as ModuleListItemData.ModuleData).moduleItems.first()
@@ -230,7 +238,8 @@ class ModuleListPresenterTest : Assert() {
)
val expectedState = moduleItemDataTemplate.copy(
title = item.title,
- iconResId = R.drawable.ic_attachment
+ iconResId = R.drawable.ic_attachment,
+ type = ModuleItem.Type.File
)
val viewState = ModuleListPresenter.present(model, context)
val itemState = (viewState.items[0] as ModuleListItemData.ModuleData).moduleItems.first()
@@ -250,7 +259,8 @@ class ModuleListPresenterTest : Assert() {
)
val expectedState = moduleItemDataTemplate.copy(
title = item.title,
- iconResId = R.drawable.ic_pages
+ iconResId = R.drawable.ic_pages,
+ type = ModuleItem.Type.Page
)
val viewState = ModuleListPresenter.present(model, context)
val itemState = (viewState.items[0] as ModuleListItemData.ModuleData).moduleItems.first()
@@ -270,7 +280,8 @@ class ModuleListPresenterTest : Assert() {
)
val expectedState = moduleItemDataTemplate.copy(
title = item.title,
- iconResId = R.drawable.ic_quiz
+ iconResId = R.drawable.ic_quiz,
+ type = ModuleItem.Type.Quiz
)
val viewState = ModuleListPresenter.present(model, context)
val itemState = (viewState.items[0] as ModuleListItemData.ModuleData).moduleItems.first()
@@ -290,7 +301,8 @@ class ModuleListPresenterTest : Assert() {
)
val expectedState = moduleItemDataTemplate.copy(
title = item.title,
- iconResId = R.drawable.ic_link
+ iconResId = R.drawable.ic_link,
+ type = ModuleItem.Type.ExternalUrl
)
val viewState = ModuleListPresenter.present(model, context)
val itemState = (viewState.items[0] as ModuleListItemData.ModuleData).moduleItems.first()
@@ -310,7 +322,8 @@ class ModuleListPresenterTest : Assert() {
)
val expectedState = moduleItemDataTemplate.copy(
title = item.title,
- iconResId = R.drawable.ic_lti
+ iconResId = R.drawable.ic_lti,
+ type = ModuleItem.Type.ExternalTool
)
val viewState = ModuleListPresenter.present(model, context)
val itemState = (viewState.items[0] as ModuleListItemData.ModuleData).moduleItems.first()
@@ -328,12 +341,7 @@ class ModuleListPresenterTest : Assert() {
moduleTemplate.copy(items = listOf(item))
)
)
- val expectedState = moduleItemDataTemplate.copy(
- title = null,
- subtitle = item.title,
- iconResId = null,
- enabled = false
- )
+ val expectedState = ModuleListItemData.SubHeader(1000L, "This is a header", 0, false, true, false)
val viewState = ModuleListPresenter.present(model, context)
val itemState = (viewState.items[0] as ModuleListItemData.ModuleData).moduleItems.first()
assertEquals(expectedState, itemState)
@@ -353,9 +361,10 @@ class ModuleListPresenterTest : Assert() {
)
val expectedState = moduleItemDataTemplate.copy(
title = item.title,
- iconResId = null,
enabled = false,
- isLoading = true
+ isLoading = true,
+ iconResId = R.drawable.ic_attachment,
+ type = ModuleItem.Type.File
)
val viewState = ModuleListPresenter.present(model, context)
val itemState = (viewState.items[0] as ModuleListItemData.ModuleData).moduleItems.first()
diff --git a/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListUpdateTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListUpdateTest.kt
index eaa44b83ea..223c874943 100644
--- a/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListUpdateTest.kt
+++ b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListUpdateTest.kt
@@ -17,9 +17,11 @@ package com.instructure.teacher.unit.modules.list
import com.instructure.canvasapi2.CanvasRestAdapter
import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.ModuleContentDetails
import com.instructure.canvasapi2.models.ModuleItem
import com.instructure.canvasapi2.models.ModuleObject
import com.instructure.canvasapi2.utils.DataResult
+import com.instructure.teacher.features.modules.list.BulkModuleUpdateAction
import com.instructure.teacher.features.modules.list.ModuleListEffect
import com.instructure.teacher.features.modules.list.ModuleListEvent
import com.instructure.teacher.features.modules.list.ModuleListModel
@@ -41,6 +43,7 @@ import io.mockk.mockkObject
import io.mockk.unmockkObject
import org.junit.Assert
import org.junit.Test
+import com.instructure.teacher.R
class ModuleListUpdateTest : Assert() {
@@ -196,7 +199,7 @@ class ModuleListUpdateTest : Assert() {
val model = initModel.copy(
modules = listOf(ModuleObject(items = items))
)
- val event = ModuleListEvent.ItemRefreshRequested("Discussion") { it.id in 1L..3L}
+ val event = ModuleListEvent.ItemRefreshRequested("Discussion") { it.id in 1L..3L }
val expectedEffect = ModuleListEffect.UpdateModuleItems(model.course, listOf(items[1]))
updateSpec
.given(model)
@@ -218,7 +221,7 @@ class ModuleListUpdateTest : Assert() {
val model = initModel.copy(
modules = listOf(ModuleObject(items = items))
)
- val event = ModuleListEvent.ItemRefreshRequested("Discussion") { it.id == 3L}
+ val event = ModuleListEvent.ItemRefreshRequested("Discussion") { it.id == 3L }
updateSpec
.given(model)
.whenEvent(event)
@@ -414,4 +417,525 @@ class ModuleListUpdateTest : Assert() {
)
}
+ @Test
+ fun `BulkUpdateModule sets loading for module`() {
+ val module = ModuleObject(id = 1L, items = listOf(ModuleItem(id = 100L, moduleId = 1L)))
+ val event = ModuleListEvent.BulkUpdateModule(1L, BulkModuleUpdateAction.PUBLISH, true)
+ val model = initModel.copy(modules = listOf(module))
+ val expectedModel = model.copy(
+ loadingModuleItemIds = setOf(1L)
+ )
+ val expectedEffect = ModuleListEffect.BulkUpdateModules(
+ expectedModel.course,
+ listOf(1L),
+ listOf(1L),
+ BulkModuleUpdateAction.PUBLISH,
+ true,
+ false
+ )
+
+ updateSpec
+ .given(model)
+ .whenEvent(event)
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateModule sets loading for module and items`() {
+ val module = ModuleObject(id = 1L, items = listOf(ModuleItem(id = 100L, moduleId = 1L)))
+ val event = ModuleListEvent.BulkUpdateModule(1L, BulkModuleUpdateAction.PUBLISH, false)
+ val model = initModel.copy(modules = listOf(module))
+ val expectedModel = model.copy(
+ loadingModuleItemIds = setOf(1L, 100L)
+ )
+ val expectedEffect = ModuleListEffect.BulkUpdateModules(
+ expectedModel.course,
+ listOf(1L),
+ listOf(1L, 100L),
+ BulkModuleUpdateAction.PUBLISH,
+ false,
+ false
+ )
+
+ updateSpec
+ .given(model)
+ .whenEvent(event)
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateAllModules sets loading for modules`() {
+ val modules = listOf(
+ ModuleObject(id = 1L, items = listOf(ModuleItem(id = 100L, moduleId = 1L))),
+ ModuleObject(
+ id = 2L, items = listOf(
+ ModuleItem(id = 200L, moduleId = 2L),
+ ModuleItem(id = 201L, moduleId = 2L)
+ )
+ )
+ )
+ val event = ModuleListEvent.BulkUpdateAllModules(BulkModuleUpdateAction.UNPUBLISH, true)
+ val model = initModel.copy(modules = modules)
+ val expectedModel = model.copy(
+ loadingModuleItemIds = setOf(1L, 2L)
+ )
+ val expectedEffect = ModuleListEffect.BulkUpdateModules(
+ expectedModel.course,
+ listOf(1L, 2L),
+ listOf(1L, 2L),
+ BulkModuleUpdateAction.UNPUBLISH,
+ true,
+ true
+ )
+
+ updateSpec
+ .given(model)
+ .whenEvent(event)
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateAllModules sets loading for modules and items`() {
+ val modules = listOf(
+ ModuleObject(id = 1L, items = listOf(ModuleItem(id = 100L, moduleId = 1L))),
+ ModuleObject(
+ id = 2L, items = listOf(
+ ModuleItem(id = 200L, moduleId = 2L),
+ ModuleItem(id = 201L, moduleId = 2L)
+ )
+ )
+ )
+ val event = ModuleListEvent.BulkUpdateAllModules(BulkModuleUpdateAction.UNPUBLISH, false)
+ val model = initModel.copy(modules = modules)
+ val expectedModel = model.copy(
+ loadingModuleItemIds = setOf(1L, 100L, 2L, 200L, 201L)
+ )
+ val expectedEffect = ModuleListEffect.BulkUpdateModules(
+ expectedModel.course,
+ listOf(1L, 2L),
+ listOf(1L, 2L, 100L, 200L, 201L),
+ BulkModuleUpdateAction.UNPUBLISH,
+ false,
+ true
+ )
+
+ updateSpec
+ .given(model)
+ .whenEvent(event)
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateSuccess emits refresh effect and clears loading`() {
+ val model = initModel.copy(
+ isLoading = false,
+ pageData = ModuleListPageData(DataResult.Success(emptyList()), false, "fakeUrl"),
+ modules = listOf(ModuleObject(1L)),
+ loadingModuleItemIds = setOf(1L)
+ )
+ val expectedModel = initModel.copy(
+ isLoading = true,
+ pageData = ModuleListPageData(forceNetwork = true),
+ loadingModuleItemIds = emptySet()
+ )
+ val expectedEffect = ModuleListEffect.LoadNextPage(
+ expectedModel.course,
+ expectedModel.pageData,
+ expectedModel.scrollToItemId
+ )
+ val expectedSnackbarEffect = ModuleListEffect.ShowSnackbar(R.string.onlyModulesPublished)
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.BulkUpdateSuccess(true, BulkModuleUpdateAction.PUBLISH, true))
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect, expectedSnackbarEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateSuccess publish all modules and items displays correct snackbar`() {
+ val model = initModel.copy(
+ isLoading = false,
+ pageData = ModuleListPageData(DataResult.Success(emptyList()), false, "fakeUrl"),
+ modules = listOf(
+ ModuleObject(1L, items = listOf(ModuleItem(100L))),
+ ModuleObject(2L, items = listOf(ModuleItem(200L)))
+ ),
+ loadingModuleItemIds = setOf(1L, 100L, 2L, 200L)
+ )
+ val expectedModel = initModel.copy(
+ isLoading = true,
+ pageData = ModuleListPageData(forceNetwork = true),
+ loadingModuleItemIds = emptySet()
+ )
+ val expectedEffect = ModuleListEffect.LoadNextPage(
+ expectedModel.course,
+ expectedModel.pageData,
+ expectedModel.scrollToItemId
+ )
+ val expectedSnackbarEffect = ModuleListEffect.ShowSnackbar(R.string.allModulesAndAllItemsPublished)
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.BulkUpdateSuccess(false, BulkModuleUpdateAction.PUBLISH, true))
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect, expectedSnackbarEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateSuccess unpublish all modules and items displays correct snackbar`() {
+ val model = initModel.copy(
+ isLoading = false,
+ pageData = ModuleListPageData(DataResult.Success(emptyList()), false, "fakeUrl"),
+ modules = listOf(
+ ModuleObject(1L, items = listOf(ModuleItem(100L))),
+ ModuleObject(2L, items = listOf(ModuleItem(200L)))
+ ),
+ loadingModuleItemIds = setOf(1L, 100L, 2L, 200L)
+ )
+ val expectedModel = initModel.copy(
+ isLoading = true,
+ pageData = ModuleListPageData(forceNetwork = true),
+ loadingModuleItemIds = emptySet()
+ )
+ val expectedEffect = ModuleListEffect.LoadNextPage(
+ expectedModel.course,
+ expectedModel.pageData,
+ expectedModel.scrollToItemId
+ )
+ val expectedSnackbarEffect = ModuleListEffect.ShowSnackbar(R.string.allModulesAndAllItemsUnpublished)
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.BulkUpdateSuccess(false, BulkModuleUpdateAction.UNPUBLISH, true))
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect, expectedSnackbarEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateSuccess publish all modules displays correct snackbar`() {
+ val model = initModel.copy(
+ isLoading = false,
+ pageData = ModuleListPageData(DataResult.Success(emptyList()), false, "fakeUrl"),
+ modules = listOf(
+ ModuleObject(1L, items = listOf(ModuleItem(100L))),
+ ModuleObject(2L, items = listOf(ModuleItem(200L)))
+ ),
+ loadingModuleItemIds = setOf(1L, 2L)
+ )
+ val expectedModel = initModel.copy(
+ isLoading = true,
+ pageData = ModuleListPageData(forceNetwork = true),
+ loadingModuleItemIds = emptySet()
+ )
+ val expectedEffect = ModuleListEffect.LoadNextPage(
+ expectedModel.course,
+ expectedModel.pageData,
+ expectedModel.scrollToItemId
+ )
+ val expectedSnackbarEffect = ModuleListEffect.ShowSnackbar(R.string.onlyModulesPublished)
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.BulkUpdateSuccess(true, BulkModuleUpdateAction.PUBLISH, true))
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect, expectedSnackbarEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateSuccess publish single module with all items displays correct snackbar`() {
+ val model = initModel.copy(
+ isLoading = false,
+ pageData = ModuleListPageData(DataResult.Success(emptyList()), false, "fakeUrl"),
+ modules = listOf(
+ ModuleObject(1L, items = listOf(ModuleItem(100L))),
+ ModuleObject(2L, items = listOf(ModuleItem(200L)))
+ ),
+ loadingModuleItemIds = setOf(1L, 100L)
+ )
+ val expectedModel = initModel.copy(
+ isLoading = true,
+ pageData = ModuleListPageData(forceNetwork = true),
+ loadingModuleItemIds = emptySet()
+ )
+ val expectedEffect = ModuleListEffect.LoadNextPage(
+ expectedModel.course,
+ expectedModel.pageData,
+ expectedModel.scrollToItemId
+ )
+ val expectedSnackbarEffect = ModuleListEffect.ShowSnackbar(R.string.moduleAndAllItemsPublished)
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.BulkUpdateSuccess(false, BulkModuleUpdateAction.PUBLISH, false))
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect, expectedSnackbarEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateSuccess unpublish single module with all items displays correct snackbar`() {
+ val model = initModel.copy(
+ isLoading = false,
+ pageData = ModuleListPageData(DataResult.Success(emptyList()), false, "fakeUrl"),
+ modules = listOf(
+ ModuleObject(1L, items = listOf(ModuleItem(100L))),
+ ModuleObject(2L, items = listOf(ModuleItem(200L)))
+ ),
+ loadingModuleItemIds = setOf(1L, 100L)
+ )
+ val expectedModel = initModel.copy(
+ isLoading = true,
+ pageData = ModuleListPageData(forceNetwork = true),
+ loadingModuleItemIds = emptySet()
+ )
+ val expectedEffect = ModuleListEffect.LoadNextPage(
+ expectedModel.course,
+ expectedModel.pageData,
+ expectedModel.scrollToItemId
+ )
+ val expectedSnackbarEffect = ModuleListEffect.ShowSnackbar(R.string.moduleAndAllItemsUnpublished)
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.BulkUpdateSuccess(false, BulkModuleUpdateAction.UNPUBLISH, false))
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect, expectedSnackbarEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateSuccess publish single module displays correct snackbar`() {
+ val model = initModel.copy(
+ isLoading = false,
+ pageData = ModuleListPageData(DataResult.Success(emptyList()), false, "fakeUrl"),
+ modules = listOf(
+ ModuleObject(1L, items = listOf(ModuleItem(100L))),
+ ModuleObject(2L, items = listOf(ModuleItem(200L)))
+ ),
+ loadingModuleItemIds = setOf(1L)
+ )
+ val expectedModel = initModel.copy(
+ isLoading = true,
+ pageData = ModuleListPageData(forceNetwork = true),
+ loadingModuleItemIds = emptySet()
+ )
+ val expectedEffect = ModuleListEffect.LoadNextPage(
+ expectedModel.course,
+ expectedModel.pageData,
+ expectedModel.scrollToItemId
+ )
+ val expectedSnackbarEffect = ModuleListEffect.ShowSnackbar(R.string.onlyModulePublished)
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.BulkUpdateSuccess(true, BulkModuleUpdateAction.PUBLISH, false))
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect, expectedSnackbarEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateFailed clears loading`() {
+ val model = initModel.copy(
+ modules = listOf(ModuleObject(1L)),
+ loadingModuleItemIds = setOf(1L)
+ )
+
+ val expectedModel = model.copy(
+ loadingModuleItemIds = emptySet()
+ )
+ val expectedSnackbarEffect = ModuleListEffect.ShowSnackbar(R.string.errorOccurred)
+
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.BulkUpdateFailed(false))
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedSnackbarEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `UpdateModuleItem emits UpdateModuleItem effect`() {
+ val model = initModel.copy(
+ modules = listOf(ModuleObject(1L, items = listOf(ModuleItem(100L)))),
+ )
+ val expectedModel = model.copy(
+ loadingModuleItemIds = setOf(100L)
+ )
+ val expectedEffect = ModuleListEffect.UpdateModuleItem(
+ model.course,
+ 1L,
+ 100L,
+ true
+ )
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.UpdateModuleItem(100L, true))
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `ModuleItemUpdateSuccess replaces module item`() {
+ val model = initModel.copy(
+ modules = listOf(ModuleObject(1L, items = listOf(ModuleItem(100L, 1L, published = false)))),
+ loadingModuleItemIds = setOf(100L)
+ )
+ val expectedModel = initModel.copy(
+ modules = listOf(ModuleObject(1L, items = listOf(ModuleItem(100L, 1L, published = true)))),
+ loadingModuleItemIds = emptySet()
+ )
+ val event = ModuleListEvent.ModuleItemUpdateSuccess(ModuleItem(100L, 1L, published = true), true)
+ updateSpec
+ .given(model)
+ .whenEvent(event)
+ .then(
+ assertThatNext(
+ hasModel(expectedModel)
+ )
+ )
+ }
+
+ @Test
+ fun `ModuleItemUpdateFailed clears loading`() {
+ val model = initModel.copy(
+ modules = listOf(ModuleObject(1L, items = listOf(ModuleItem(100L, 1L, published = false)))),
+ loadingModuleItemIds = setOf(100L)
+ )
+ val expectedModel = model.copy(
+ loadingModuleItemIds = emptySet()
+ )
+ val event = ModuleListEvent.ModuleItemUpdateFailed(100L)
+ updateSpec
+ .given(model)
+ .whenEvent(event)
+ .then(
+ assertThatNext(
+ hasModel(expectedModel)
+ )
+ )
+ }
+
+ @Test
+ fun `UpdateFileModuleItem emits UpdateFileModuleItem effect`() {
+ val model = initModel.copy(
+ modules = listOf(ModuleObject(1L, items = listOf(ModuleItem(100L, 1L, published = false)))),
+ )
+ val expectedEffect = ModuleListEffect.UpdateFileModuleItem(
+ 100L,
+ ModuleContentDetails()
+ )
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.UpdateFileModuleItem(100L, ModuleContentDetails()))
+ .then(
+ assertThatNext(
+ matchesEffects(expectedEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `BulkUpdateCancelled emits LoadNextPage effect`() {
+ val model = initModel.copy(
+ isLoading = false,
+ pageData = ModuleListPageData(DataResult.Success(emptyList()), false, "fakeUrl"),
+ modules = listOf(
+ ModuleObject(1L, items = listOf(ModuleItem(100L))),
+ ModuleObject(2L, items = listOf(ModuleItem(200L)))
+ ),
+ loadingModuleItemIds = setOf(1L)
+ )
+ val expectedModel = initModel.copy(
+ isLoading = true,
+ pageData = ModuleListPageData(forceNetwork = true),
+ loadingModuleItemIds = emptySet()
+ )
+ val expectedEffect = ModuleListEffect.LoadNextPage(
+ expectedModel.course,
+ expectedModel.pageData,
+ expectedModel.scrollToItemId
+ )
+ val snackbarEffect = ModuleListEffect.ShowSnackbar(R.string.updateCancelled)
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.BulkUpdateCancelled)
+ .then(
+ assertThatNext(
+ hasModel(expectedModel),
+ matchesEffects(expectedEffect, snackbarEffect)
+ )
+ )
+ }
+
+ @Test
+ fun `ShowSnackbar event emits ShowSnackbar effect`() {
+ val model = initModel.copy(
+ isLoading = false,
+ pageData = ModuleListPageData(DataResult.Success(emptyList()), false, "fakeUrl"),
+ modules = listOf(
+ ModuleObject(1L, items = listOf(ModuleItem(100L))),
+ ModuleObject(2L, items = listOf(ModuleItem(200L)))
+ ),
+ loadingModuleItemIds = setOf(1L)
+ )
+ val params = arrayOf("param")
+ val snackbarEffect = ModuleListEffect.ShowSnackbar(R.string.error_unpublishable_module_item, params)
+ updateSpec
+ .given(model)
+ .whenEvent(ModuleListEvent.ShowSnackbar(R.string.error_unpublishable_module_item, params))
+ .then(
+ assertThatNext(
+ matchesEffects(snackbarEffect)
+ )
+ )
+ }
+
+
}
diff --git a/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/file/UpdateFileViewModelTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/file/UpdateFileViewModelTest.kt
new file mode 100644
index 0000000000..16a3f38d19
--- /dev/null
+++ b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/file/UpdateFileViewModelTest.kt
@@ -0,0 +1,647 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.instructure.teacher.unit.modules.list.file
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.lifecycle.SavedStateHandle
+import com.instructure.canvasapi2.apis.FileFolderAPI
+import com.instructure.canvasapi2.models.FileFolder
+import com.instructure.canvasapi2.models.ModuleContentDetails
+import com.instructure.canvasapi2.models.UpdateFileFolder
+import com.instructure.canvasapi2.utils.DataResult
+import com.instructure.canvasapi2.utils.DateHelper
+import com.instructure.canvasapi2.utils.toApiString
+import com.instructure.pandautils.mvvm.ViewState
+import com.instructure.pandautils.utils.FileFolderUpdatedEvent
+import com.instructure.teacher.features.modules.list.ui.file.FileAvailability
+import com.instructure.teacher.features.modules.list.ui.file.FileVisibility
+import com.instructure.teacher.features.modules.list.ui.file.UpdateFileEvent
+import com.instructure.teacher.features.modules.list.ui.file.UpdateFileViewModel
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.slot
+import io.mockk.unmockkAll
+import io.mockk.verify
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNull
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.setMain
+import org.greenrobot.eventbus.EventBus
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+
+@ExperimentalCoroutinesApi
+class UpdateFileViewModelTest {
+
+ @get:Rule
+ var instantExecutorRule = InstantTaskExecutorRule()
+
+ private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true)
+ private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner)
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+
+ private val savedStateHandle: SavedStateHandle = mockk(relaxed = true)
+ private val context: Context = mockk(relaxed = true)
+ private val fileApi: FileFolderAPI.FilesFoldersInterface = mockk(relaxed = true)
+ private val eventBus: EventBus = mockk(relaxed = true)
+
+ private lateinit var viewModel: UpdateFileViewModel
+
+ private val dateFormat = SimpleDateFormat("yyyy-MM-dd")
+ private val timeFormat = SimpleDateFormat("HH:mm")
+
+ @Before
+ fun setUp() {
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ Dispatchers.setMain(testDispatcher)
+
+ mockkObject(DateHelper)
+ val dateCaptor = slot()
+ every { DateHelper.getFormattedDate(any(), capture(dateCaptor)) } answers {
+ dateFormat.format(dateCaptor.captured)
+ }
+ val timeCaptor = slot()
+ every { DateHelper.getFormattedTime(any(), capture(timeCaptor)) } answers {
+ timeFormat.format(timeCaptor.captured)
+ }
+ }
+
+ @After
+ fun teardown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `loadData sets correct state`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ assertEquals(ViewState.Success, viewModel.state.value)
+ }
+
+ @Test
+ fun `Error during file fetching sets visibility to Inherit`() {
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Fail()
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ assertEquals(FileVisibility.INHERIT, viewModel.data.value?.selectedVisibility)
+ assert(viewModel.state.value is ViewState.Success)
+ }
+
+ @Test
+ fun `Published file maps correctly`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ assertEquals(FileAvailability.PUBLISHED, viewModel.data.value?.selectedAvailability)
+ assert(viewModel.state.value is ViewState.Success)
+ }
+
+ @Test
+ fun `Unpublished file maps correctly`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = true)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ assertEquals(FileAvailability.UNPUBLISHED, viewModel.data.value?.selectedAvailability)
+ }
+
+ @Test
+ fun `Hidden file maps correctly`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = true, locked = false)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ assertEquals(FileAvailability.HIDDEN, viewModel.data.value?.selectedAvailability)
+ }
+
+ @Test
+ fun `Scheduled file maps correctly`() {
+ val calendar = Calendar.getInstance()
+ val unlockDate = calendar.time
+ calendar.add(Calendar.DAY_OF_YEAR, 1)
+ val lockDate = calendar.time
+
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(
+ hidden = false,
+ locked = false,
+ lockAt = lockDate.toApiString(),
+ unlockAt = unlockDate.toApiString()
+ )
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ assertEquals(FileAvailability.SCHEDULED, viewModel.data.value?.selectedAvailability)
+ assertEquals(dateFormat.format(lockDate), viewModel.data.value?.lockAtDateString)
+ assertEquals(timeFormat.format(lockDate), viewModel.data.value?.lockAtTimeString)
+ assertEquals(dateFormat.format(unlockDate), viewModel.data.value?.unlockAtDateString)
+ assertEquals(timeFormat.format(unlockDate), viewModel.data.value?.unlockAtTimeString)
+ }
+
+ @Test
+ fun `Inherit visibility maps correctly`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.INHERIT.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ assertEquals(FileVisibility.INHERIT, viewModel.data.value?.selectedVisibility)
+ }
+
+ @Test
+ fun `Context visibility maps correctly`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.CONTEXT.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ assertEquals(FileVisibility.CONTEXT, viewModel.data.value?.selectedVisibility)
+ }
+
+ @Test
+ fun `Institution visibility maps correctly`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.INSTITUTION.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ assertEquals(FileVisibility.INSTITUTION, viewModel.data.value?.selectedVisibility)
+ }
+
+ @Test
+ fun `Public visibility maps correctly`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ assertEquals(FileVisibility.PUBLIC, viewModel.data.value?.selectedVisibility)
+ }
+
+ @Test
+ fun `Availability change updates data`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.onAvailabilityChanged(FileAvailability.HIDDEN)
+
+ assertEquals(FileAvailability.HIDDEN, viewModel.data.value?.selectedAvailability)
+ }
+
+ @Test
+ fun `Visibility change updates data`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.onVisibilityChanged(FileVisibility.CONTEXT)
+
+ assertEquals(FileVisibility.CONTEXT, viewModel.data.value?.selectedVisibility)
+ }
+
+ @Test
+ fun `Close emits correct event`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false)
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel = createViewModel()
+ viewModel.close()
+
+ assertEquals(UpdateFileEvent.Close, viewModel.events.value?.getContentIfNotHandled())
+ }
+
+ @Test
+ fun `Update lock at date`() {
+ val calendar = Calendar.getInstance()
+ calendar.add(Calendar.DAY_OF_YEAR, -1)
+ calendar.set(Calendar.HOUR_OF_DAY, 0)
+ calendar.set(Calendar.MINUTE, 0)
+
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false, lockAt = Date().toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.updateLockAt()
+ val event = viewModel.events.value?.getContentIfNotHandled()
+ assert(event is UpdateFileEvent.ShowDatePicker)
+ (event as UpdateFileEvent.ShowDatePicker).callback(
+ calendar.get(Calendar.YEAR),
+ calendar.get(Calendar.MONTH),
+ calendar.get(Calendar.DAY_OF_MONTH)
+ )
+
+ assertEquals(dateFormat.format(calendar.time), viewModel.data.value?.lockAtDateString)
+ assertEquals(timeFormat.format(calendar.time), viewModel.data.value?.lockAtTimeString)
+ }
+
+ @Test
+ fun `Update lock at time`() {
+ val calendar = Calendar.getInstance()
+
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false, lockAt = calendar.time.toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.updateLockTime()
+ val event = viewModel.events.value?.getContentIfNotHandled()
+ assert(event is UpdateFileEvent.ShowTimePicker)
+
+ calendar.add(Calendar.HOUR_OF_DAY, 1)
+ calendar.add(Calendar.MINUTE, 1)
+ (event as UpdateFileEvent.ShowTimePicker).callback(
+ calendar.get(Calendar.HOUR_OF_DAY),
+ calendar.get(Calendar.MINUTE)
+ )
+
+ assertEquals(dateFormat.format(calendar.time), viewModel.data.value?.lockAtDateString)
+ assertEquals(timeFormat.format(calendar.time), viewModel.data.value?.lockAtTimeString)
+ }
+
+ @Test
+ fun `Update unlock at date`() {
+ val calendar = Calendar.getInstance()
+ calendar.add(Calendar.DAY_OF_YEAR, -1)
+ calendar.set(Calendar.HOUR_OF_DAY, 0)
+ calendar.set(Calendar.MINUTE, 0)
+
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false, unlockAt = Date().toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.updateUnlockAt()
+ val event = viewModel.events.value?.getContentIfNotHandled()
+ assert(event is UpdateFileEvent.ShowDatePicker)
+ (event as UpdateFileEvent.ShowDatePicker).callback(
+ calendar.get(Calendar.YEAR),
+ calendar.get(Calendar.MONTH),
+ calendar.get(Calendar.DAY_OF_MONTH)
+ )
+
+ assertEquals(dateFormat.format(calendar.time), viewModel.data.value?.unlockAtDateString)
+ assertEquals(timeFormat.format(calendar.time), viewModel.data.value?.unlockAtTimeString)
+ }
+
+ @Test
+ fun `Update unlock at time`() {
+ val calendar = Calendar.getInstance()
+
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ val contentDetails =
+ ModuleContentDetails(hidden = false, locked = false, unlockAt = calendar.time.toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.updateUnlockTime()
+ val event = viewModel.events.value?.getContentIfNotHandled()
+ assert(event is UpdateFileEvent.ShowTimePicker)
+
+ calendar.add(Calendar.HOUR_OF_DAY, 1)
+ calendar.add(Calendar.MINUTE, 1)
+ (event as UpdateFileEvent.ShowTimePicker).callback(
+ calendar.get(Calendar.HOUR_OF_DAY),
+ calendar.get(Calendar.MINUTE)
+ )
+
+ assertEquals(dateFormat.format(calendar.time), viewModel.data.value?.unlockAtDateString)
+ assertEquals(timeFormat.format(calendar.time), viewModel.data.value?.unlockAtTimeString)
+ }
+
+ @Test
+ fun `Clear lock time`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false, lockAt = Date().toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.clearLockDate()
+
+ assertNull(viewModel.data.value?.lockAt)
+ assertNull(viewModel.data.value?.lockAtDateString)
+ assertNull(viewModel.data.value?.lockAtTimeString)
+ }
+
+ @Test
+ fun `Clear unlock time`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false, unlockAt = Date().toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.clearUnlockDate()
+
+ assertNull(viewModel.data.value?.unlockAt)
+ assertNull(viewModel.data.value?.unlockAtDateString)
+ assertNull(viewModel.data.value?.unlockAtTimeString)
+ }
+
+ @Test
+ fun `Hiding file calls api with correct params`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ coEvery { fileApi.updateFile(any(), any(), any()) } returns DataResult.Success(file)
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false, unlockAt = Date().toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.onAvailabilityChanged(FileAvailability.HIDDEN)
+
+ viewModel.update()
+
+ coVerify {
+ fileApi.updateFile(
+ 1L,
+ UpdateFileFolder(
+ hidden = true,
+ unlockAt = "",
+ lockAt = "",
+ locked = false,
+ visibilityLevel = FileVisibility.PUBLIC.name.lowercase()
+ ),
+ any()
+ )
+ }
+ }
+
+ @Test
+ fun `Unpublishing file calls api with correct params`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.PUBLIC.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ coEvery { fileApi.updateFile(any(), any(), any()) } returns DataResult.Success(file)
+ val contentDetails = ModuleContentDetails(hidden = false, locked = false, unlockAt = Date().toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.onAvailabilityChanged(FileAvailability.UNPUBLISHED)
+
+ viewModel.update()
+
+ coVerify {
+ fileApi.updateFile(
+ 1L,
+ UpdateFileFolder(
+ hidden = false,
+ unlockAt = "",
+ lockAt = "",
+ locked = true,
+ visibilityLevel = FileVisibility.PUBLIC.name.lowercase()
+ ),
+ any()
+ )
+ }
+ }
+
+ @Test
+ fun `Publishing file calls api with correct params`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.CONTEXT.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ coEvery { fileApi.updateFile(any(), any(), any()) } returns DataResult.Success(file)
+ val contentDetails = ModuleContentDetails(hidden = false, locked = true, unlockAt = Date().toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.onAvailabilityChanged(FileAvailability.PUBLISHED)
+
+ viewModel.update()
+
+ coVerify {
+ fileApi.updateFile(
+ 1L,
+ UpdateFileFolder(
+ hidden = false,
+ unlockAt = "",
+ lockAt = "",
+ locked = false,
+ visibilityLevel = FileVisibility.CONTEXT.name.lowercase()
+ ),
+ any()
+ )
+ }
+ }
+
+ @Test
+ fun `Updating file emits correct events`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.CONTEXT.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ coEvery { fileApi.updateFile(any(), any(), any()) } returns DataResult.Success(file)
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = true, unlockAt = Date().toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.onAvailabilityChanged(FileAvailability.PUBLISHED)
+
+ viewModel.update()
+
+ val event = viewModel.events.value?.getContentIfNotHandled()
+ assert(event is UpdateFileEvent.Close)
+ verify {
+ eventBus.post(any())
+ }
+ }
+
+ @Test
+ fun `Update error sets correct view state`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.CONTEXT.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ coEvery { fileApi.updateFile(any(), any(), any()) } returns DataResult.Fail()
+
+ val contentDetails = ModuleContentDetails(hidden = false, locked = true, unlockAt = Date().toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.onAvailabilityChanged(FileAvailability.PUBLISHED)
+
+ viewModel.update()
+
+ assert(viewModel.state.value is ViewState.Error)
+ }
+
+ @Test
+ fun `minDate is set if unlockDate is not null`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.CONTEXT.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ val contentDetails = ModuleContentDetails(hidden = false, locked = true, unlockAt = Date().toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.updateLockAt()
+
+ val event = viewModel.events.value?.getContentIfNotHandled()
+ assert(event is UpdateFileEvent.ShowDatePicker)
+ assertEquals(contentDetails.unlockDate, (event as UpdateFileEvent.ShowDatePicker).minDate)
+ }
+
+ @Test
+ fun `maxDate is set if lockDate is not null`() {
+ val file = FileFolder(id = 1L, visibilityLevel = FileVisibility.CONTEXT.name.lowercase())
+ coEvery { fileApi.getFile(any(), any()) } returns DataResult.Success(file)
+ val contentDetails = ModuleContentDetails(hidden = false, locked = true, unlockAt = Date().toApiString())
+
+ every { savedStateHandle.get("contentId") } returns 1L
+ every { savedStateHandle.get("contentDetails") } returns contentDetails
+
+ viewModel = createViewModel()
+
+ viewModel.updateUnlockAt()
+
+ val event = viewModel.events.value?.getContentIfNotHandled()
+ assert(event is UpdateFileEvent.ShowDatePicker)
+ assertEquals(contentDetails.lockDate, (event as UpdateFileEvent.ShowDatePicker).maxDate)
+ }
+
+ private fun createViewModel() = UpdateFileViewModel(savedStateHandle, context, fileApi, eventBus)
+}
\ No newline at end of file
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/AssignmentGroupsApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/AssignmentGroupsApi.kt
index 6bc4513023..82f37c068a 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/AssignmentGroupsApi.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/AssignmentGroupsApi.kt
@@ -34,7 +34,7 @@ object AssignmentGroupsApi {
private fun assignmentGroupsService(token: String): AssignmentGroupsService
= CanvasNetworkAdapter.retrofitWithToken(token).create(AssignmentGroupsService::class.java)
- fun createAssignmentGroup(token: String, courseId: Long, name: String, position: Int?, groupWeight: Int?, sisSourceId: Long?): AssignmentGroupApiModel {
+ fun createAssignmentGroup(token: String, courseId: Long, name: String, position: Int? = null, groupWeight: Int? = null, sisSourceId: Long? = null): AssignmentGroupApiModel {
val assignmentGroup = CreateAssignmentGroup(name, position, groupWeight, sisSourceId)
return assignmentGroupsService(token)
.createAssignmentGroup(courseId, assignmentGroup)
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/AssignmentsApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/AssignmentsApi.kt
index 15b030191b..b665471602 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/AssignmentsApi.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/AssignmentsApi.kt
@@ -39,23 +39,24 @@ object AssignmentsApi {
= CanvasNetworkAdapter.retrofitWithToken(token).create(AssignmentsService::class.java)
data class CreateAssignmentRequest(
- val courseId : Long,
- val withDescription: Boolean = false,
- val lockAt: String = "",
- val unlockAt: String = "",
- val dueAt: String = "",
- val submissionTypes: List,
- val gradingType: GradingType = GradingType.POINTS,
- val allowedExtensions: List? = null,
- val teacherToken: String,
- val groupCategoryId: Long? = null,
- val pointsPossible: Double? = null,
- val importantDate: Boolean? = null,
- val assignmentGroupId: Long? = null)
+ val courseId: Long,
+ val withDescription: Boolean = false,
+ val lockAt: String = "",
+ val unlockAt: String = "",
+ val dueAt: String = "",
+ val submissionTypes: List,
+ val gradingType: GradingType = GradingType.POINTS,
+ val allowedExtensions: List? = null,
+ val teacherToken: String,
+ val groupCategoryId: Long? = null,
+ val pointsPossible: Double? = null,
+ val importantDate: Boolean? = null,
+ val assignmentGroupId: Long? = null)
fun createAssignment(request: CreateAssignmentRequest): AssignmentApiModel {
return createAssignment(
request.courseId,
+ request.teacherToken,
request.withDescription,
request.lockAt,
request.unlockAt,
@@ -63,7 +64,6 @@ object AssignmentsApi {
request.submissionTypes,
request.gradingType,
request.allowedExtensions,
- request.teacherToken,
request.groupCategoryId,
request.pointsPossible,
request.importantDate,
@@ -72,19 +72,19 @@ object AssignmentsApi {
}
fun createAssignment(
- courseId: Long,
- withDescription: Boolean,
- lockAt: String,
- unlockAt: String,
- dueAt: String,
- submissionTypes: List,
- gradingType: GradingType,
- allowedExtensions: List?,
- teacherToken: String,
- groupCategoryId: Long?,
- pointsPossible: Double?,
- importantDate: Boolean?,
- assignmentGroupId: Long?): AssignmentApiModel {
+ courseId: Long,
+ teacherToken: String,
+ withDescription: Boolean = false,
+ lockAt: String = "",
+ unlockAt: String = "",
+ dueAt: String = "",
+ submissionTypes: List = emptyList(),
+ gradingType: GradingType = GradingType.POINTS,
+ allowedExtensions: List? = null,
+ groupCategoryId: Long? = null,
+ pointsPossible: Double? = null,
+ importantDate: Boolean? = null,
+ assignmentGroupId: Long? = null): AssignmentApiModel {
val assignment = CreateAssignmentWrapper(Randomizer.randomAssignment(
withDescription,
lockAt,
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConferencesApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConferencesApi.kt
index 3359cf5004..d00fa68907 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConferencesApi.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConferencesApi.kt
@@ -4,6 +4,7 @@ import com.instructure.dataseeding.model.ConferencesRequestApiModel
import com.instructure.dataseeding.model.ConferencesResponseApiModel
import com.instructure.dataseeding.model.WebConferenceWrapper
import com.instructure.dataseeding.util.CanvasNetworkAdapter
+import com.instructure.dataseeding.util.Randomizer
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
@@ -19,14 +20,14 @@ object ConferencesApi {
private fun conferencesService(token: String): ConferencesService
= CanvasNetworkAdapter.retrofitWithToken(token).create(ConferencesService::class.java)
- fun createCourseConference(token: String, title: String, description: String, conferenceType: String, longRunning: Boolean, duration: Int, userIds: List, courseId: Long): ConferencesResponseApiModel {
+ fun createCourseConference(courseId: Long, token: String, title: String = Randomizer.randomConferenceTitle(), description: String = Randomizer.randomConferenceDescription(), conferenceType: String = "BigBlueButton", longRunning: Boolean = false, duration: Int = 70, recipientUserIds: List): ConferencesResponseApiModel {
val conference = WebConferenceWrapper(webConference = ConferencesRequestApiModel(
title,
description,
conferenceType,
longRunning,
duration,
- userIds)
+ recipientUserIds)
)
return conferencesService(token).createCourseConference(courseId, conference).execute().body()!!
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConversationsApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConversationsApi.kt
index fc2989bd96..5885fa8caf 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConversationsApi.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConversationsApi.kt
@@ -44,7 +44,7 @@ object ConversationsApi {
}
private fun conversationsService(token: String): ConversationsService
- = CanvasNetworkAdapter.retrofitWithToken(token).create(ConversationsApi.ConversationsService::class.java)
+ = CanvasNetworkAdapter.retrofitWithToken(token).create(ConversationsService::class.java)
fun createConversation(token: String, recipients: List, subject: String = Randomizer.randomConversationSubject(), body: String = Randomizer.randomConversationBody()): List {
val conversation = CreateConversation(recipients, subject, body)
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/EnrollmentsApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/EnrollmentsApi.kt
index 1365fc0705..c2dc5eb811 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/EnrollmentsApi.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/EnrollmentsApi.kt
@@ -57,7 +57,7 @@ object EnrollmentsApi {
= enrollUser(courseId, userId, TA_ENROLLMENT)
fun enrollUserAsObserver(courseId: Long, userId: Long, associatedUserId: Long): EnrollmentApiModel
- = enrollUser(courseId, userId, OBSERVER_ENROLLMENT, associatedUserId.takeIf { it > 0 })
+ = enrollUser(courseId, userId, OBSERVER_ENROLLMENT, associatedUserId = associatedUserId.takeIf { it > 0 })
fun enrollUserAsDesigner(courseId: Long, userId: Long): EnrollmentApiModel
= enrollUser(courseId, userId, DESIGNER_ENROLLMENT)
@@ -66,8 +66,8 @@ object EnrollmentsApi {
courseId: Long,
userId: Long,
enrollmentType: String,
- associatedUserId: Long? = null,
- enrollmentService: EnrollmentsService = enrollmentsService
+ enrollmentService: EnrollmentsService = enrollmentsService,
+ associatedUserId: Long? = null
): EnrollmentApiModel {
val enrollment = EnrollmentApiRequestModel(userId, enrollmentType, enrollmentType, associatedUserId = associatedUserId)
return enrollmentService
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/FileFolderApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/FileFolderApi.kt
index 8207e383df..5d3d1663bd 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/FileFolderApi.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/FileFolderApi.kt
@@ -48,16 +48,20 @@ object FileFolderApi {
.body()!!
}
- fun createCourseFolder(folderId: Long, name: String, locked: Boolean, token: String): CourseFolderUploadApiModel {
+ fun createCourseFolder(
+ folderId: Long,
+ token: String,
+ name: String,
+ locked: Boolean = false
+ ): CourseFolderUploadApiModel {
val courseFolderUploadRequestModel = CourseFolderUploadApiRequestModel(
name = name,
locked = locked
)
- val createFolderUploadApiModel: CourseFolderUploadApiModel = fileFolderService(token)
+ return fileFolderService(token)
.createCourseFolder(folderId, courseFolderUploadRequestModel)
.execute()
.body()!!
- return createFolderUploadApiModel
}
}
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ModulesApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ModulesApi.kt
index ac428b1ebc..80673ea634 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ModulesApi.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ModulesApi.kt
@@ -34,28 +34,28 @@ object ModulesApi {
private fun modulesService(token: String): ModulesService
= CanvasNetworkAdapter.retrofitWithToken(token).create(ModulesService::class.java)
- fun createModule(courseId: Long, teacherToken: String, unlockAt: String?): ModuleApiModel {
+ fun createModule(courseId: Long, teacherToken: String, unlockAt: String? = null): ModuleApiModel {
val module = CreateModuleWrapper(Randomizer.createModule(unlockAt))
return modulesService(teacherToken).createModules(courseId, module).execute().body()!!
}
// Canvas API does not support creating a published module.
// All modules must be created and published in separate calls.
- fun updateModule(courseId: Long, id: Long, published: Boolean, teacherToken: String): ModuleApiModel {
+ fun updateModule(courseId: Long, teacherToken: String, moduleId: Long, published: Boolean = true): ModuleApiModel {
val update = UpdateModuleWrapper(UpdateModule(published))
- return modulesService(teacherToken).updateModule(courseId, id, update).execute().body()!!
+ return modulesService(teacherToken).updateModule(courseId, moduleId, update).execute().body()!!
}
fun createModuleItem(
- courseId: Long,
- moduleId: Long,
- teacherToken: String,
- title: String,
- type: String,
- contentId: String?,
- pageUrl: String? = null
+ courseId: Long,
+ teacherToken: String,
+ moduleId: Long,
+ moduleItemTitle: String,
+ moduleItemType: String,
+ contentId: String? = null,
+ pageUrl: String? = null
): ModuleItemApiModel {
- val newItem = CreateModuleItem(title, type, contentId, pageUrl)
+ val newItem = CreateModuleItem(moduleItemTitle, moduleItemType, contentId, pageUrl)
val newItemWrapper = CreateModuleItemWrapper(moduleItem = newItem)
return modulesService(teacherToken).createModuleItem(
courseId = courseId,
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/PagesApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/PagesApi.kt
index 32e480f9e4..5c5353a4a0 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/PagesApi.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/PagesApi.kt
@@ -39,12 +39,12 @@ object PagesApi {
fun createCoursePage(
courseId: Long,
- published: Boolean,
- frontPage: Boolean,
- editingRoles: String? = null,
token: String,
- body: String = Randomizer.randomPageBody()
- ): PageApiModel {
+ published: Boolean = true,
+ frontPage: Boolean = false,
+ body: String = Randomizer.randomPageBody(),
+ editingRoles: String? = null,
+ ): PageApiModel {
val page = CreatePageWrapper(CreatePage(Randomizer.randomPageTitle(), body, published, frontPage, editingRoles))
return pagesService(token)
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/QuizzesApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/QuizzesApi.kt
index 91c0c36af9..13b575ed2d 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/QuizzesApi.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/QuizzesApi.kt
@@ -80,12 +80,13 @@ object QuizzesApi {
fun createQuiz(
courseId: Long,
- withDescription: Boolean,
- lockAt: String,
- unlockAt: String,
- dueAt: String,
- published: Boolean,
- token: String): QuizApiModel {
+ token: String,
+ withDescription: Boolean = true,
+ lockAt: String = "",
+ unlockAt: String = "",
+ dueAt: String = "",
+ published: Boolean = true
+ ): QuizApiModel {
val quiz = CreateQuiz(Randomizer.randomQuiz(withDescription, lockAt, unlockAt, dueAt, published))
return quizzesService(token)
@@ -178,7 +179,7 @@ object QuizzesApi {
val result = QuizListApiModel(
quizList = (0 until numQuizzes).map {
- QuizzesApi.createQuiz(request)
+ createQuiz(request)
}
)
@@ -208,7 +209,7 @@ object QuizzesApi {
// Convenience method to create and publish a quiz with questions
fun createAndPublishQuiz(courseId: Long, teacherToken: String, questions: List) : QuizApiModel {
- val result = QuizzesApi.createQuiz(QuizzesApi.CreateQuizRequest(
+ val result = createQuiz(CreateQuizRequest(
courseId = courseId,
withDescription = true,
published = false, // Will publish in just a bit, after we add questions
@@ -216,7 +217,7 @@ object QuizzesApi {
))
for(question in questions) {
- val result = QuizzesApi.createQuizQuestion(
+ val result = createQuizQuestion(
courseId = courseId,
quizId = result.id,
teacherToken = teacherToken,
@@ -225,7 +226,7 @@ object QuizzesApi {
question.id = result.id // back-fill the question id
}
- QuizzesApi.publishQuiz(
+ publishQuiz(
courseId = courseId,
quizId = result.id,
teacherToken = teacherToken,
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/SubmissionsApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/SubmissionsApi.kt
index ca8fe7fac6..4f11a7a1e7 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/SubmissionsApi.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/SubmissionsApi.kt
@@ -59,11 +59,12 @@ object SubmissionsApi {
private fun submissionsService(token: String): SubmissionsService
= CanvasNetworkAdapter.retrofitWithToken(token).create(SubmissionsService::class.java)
- fun submitCourseAssignment(submissionType: SubmissionType,
- courseId: Long,
+ fun submitCourseAssignment(courseId: Long,
+ studentToken: String,
assignmentId: Long,
- fileIds: MutableList,
- studentToken: String): SubmissionApiModel {
+ submissionType: SubmissionType,
+ fileIds: MutableList = mutableListOf()
+ ): SubmissionApiModel {
val submission = Randomizer.randomSubmission(submissionType, fileIds)
@@ -73,10 +74,10 @@ object SubmissionsApi {
.body()!!
}
- fun commentOnSubmission(studentToken: String,
- courseId: Long,
+ fun commentOnSubmission(courseId: Long,
+ studentToken: String,
assignmentId: Long,
- fileIds: MutableList,
+ fileIds: MutableList = mutableListOf(),
attempt: Int = 1): AssignmentApiModel {
val comment = Randomizer.randomSubmissionComment(fileIds, attempt)
@@ -101,8 +102,8 @@ object SubmissionsApi {
courseId: Long,
assignmentId: Long,
studentId: Long,
- postedGrade: String? = null,
- excused: Boolean): SubmissionApiModel {
+ excused: Boolean = false,
+ postedGrade: String? = null): SubmissionApiModel {
return submissionsService(teacherToken)
.gradeSubmission(courseId, assignmentId, studentId, GradeSubmissionWrapper(GradeSubmission(postedGrade, excused)))
@@ -144,70 +145,69 @@ object SubmissionsApi {
/** Seed one or more submissions for an assignment. Accepts a SubmissionSeedRequest, returns a
* list of SubmissionApiModel objects.
*/
- fun seedAssignmentSubmission(request: SubmissionSeedRequest) : List {
+ fun seedAssignmentSubmission(courseId: Long, studentToken: String, assignmentId: Long, commentSeedsList: List = listOf(), submissionSeedsList: List = listOf()): List {
val submissionsList = mutableListOf()
- with(request) {
- for (seed in submissionSeedsList) {
- for (t in 0 until seed.amount) {
-
- // Submit an assignment
-
- // Canvas will only record submissions with unique "submitted_at" values.
- // Sleep for 1 second to ensure submissions are recorded!!!
- //
- // https://github.com/instructure/mobile_qa/blob/7f985a08161f457e9b5d60987bd6278d21e2557e/SoSeedy/lib/so_seedy/canvas_models/account_admin.rb#L357-L359
- Thread.sleep(1000)
- var submission = submitCourseAssignment(
- submissionType = seed.submissionType,
- courseId = courseId,
- assignmentId = assignmentId,
- fileIds = seed.attachmentsList.map { it.id }.toMutableList(),
- studentToken = studentToken
- )
-
- if (seed.checkForLateStatus) {
- val maxAttempts = 6
- var attempts = 1
- while (attempts < maxAttempts) {
- val submissionResponse = getSubmission (
+
+ for (seed in submissionSeedsList) {
+ for (t in 0 until seed.amount) {
+
+ // Submit an assignment
+
+ // Canvas will only record submissions with unique "submitted_at" values.
+ // Sleep for 1 second to ensure submissions are recorded!!!
+ //
+ // https://github.com/instructure/mobile_qa/blob/7f985a08161f457e9b5d60987bd6278d21e2557e/SoSeedy/lib/so_seedy/canvas_models/account_admin.rb#L357-L359
+ Thread.sleep(1000)
+ var submission = submitCourseAssignment(
+ submissionType = seed.submissionType,
+ courseId = courseId,
+ assignmentId = assignmentId,
+ fileIds = seed.attachmentsList.map { it.id }.toMutableList(),
+ studentToken = studentToken
+ )
+
+ if (seed.checkForLateStatus) {
+ val maxAttempts = 6
+ var attempts = 1
+ while (attempts < maxAttempts) {
+ val submissionResponse = getSubmission (
+ studentToken = studentToken,
+ courseId = courseId,
+ assignmentId = assignmentId,
+ studentId = submission.userId
+ )
+ if (submissionResponse.late) break
+ RetryBackoff.wait(attempts)
+ attempts++
+ }
+ }
+
+ // Create comments on the submitted assignment
+ submission = commentSeedsList
+ .map {
+ // Create comments with any assigned upload file types
+ val assignment = commentOnSubmission(
studentToken = studentToken,
courseId = courseId,
assignmentId = assignmentId,
- studentId = submission.userId
+ fileIds = it.attachmentsList.filter { it.id != -1L }.map { it.id }.toMutableList())
+
+ // Apparently, we only care about id and submissionComments
+ SubmissionApiModel(
+ id = assignment.id,
+ submissionComments = assignment.submissionComments!!,
+ url = null,
+ body = null,
+ userId = 0,
+ grade = null,
+ attempt = assignment.attempt!!
+
)
- if (submissionResponse.late) break
- RetryBackoff.wait(attempts)
- attempts++
}
- }
+ .lastOrNull() ?: submission // Last one (if it exists) will have all the comments loaded up on it
- // Create comments on the submitted assignment
- submission = commentSeedsList
- .map {
- // Create comments with any assigned upload file types
- val assignment = commentOnSubmission(
- studentToken = studentToken,
- courseId = courseId,
- assignmentId = assignmentId,
- fileIds = it.attachmentsList.filter { it.id != -1L }.map { it.id }.toMutableList())
-
- // Apparently, we only care about id and submissionComments
- SubmissionApiModel(
- id = assignment.id,
- submissionComments = assignment.submissionComments!!,
- url = null,
- body = null,
- userId = 0,
- grade = null,
- attempt = assignment.attempt!!
-
- )
- }
- .lastOrNull() ?: submission // Last one (if it exists) will have all the comments loaded up on it
-
- // Add submission to our collection
- submissionsList.add(submission)
- }
+ // Add submission to our collection
+ submissionsList.add(submission)
}
}
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/UserApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/UserApi.kt
index 4ad56323f6..320922357e 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/UserApi.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/UserApi.kt
@@ -63,7 +63,7 @@ object UserApi {
userAdminService.putSelfSettings(userId, requestApiModel).execute()
}
- fun createCanvasUser(
+ fun createCanvasUser(
userService: UserService = userAdminService,
userDomain: String = CanvasNetworkAdapter.canvasDomain
): CanvasUserApiModel {
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/DiscussionApiModel.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/DiscussionApiModel.kt
index ccfbae6996..8bc6092a40 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/DiscussionApiModel.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/DiscussionApiModel.kt
@@ -28,7 +28,9 @@ data class DiscussionApiModel (
@SerializedName("locked_for_user")
val lockedForUser: Boolean,
@SerializedName("locked")
- val locked: Boolean
+ val locked: Boolean,
+ @SerializedName("published")
+ val published: Boolean? = true
)
data class CreateDiscussionTopic(
diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/Randomizer.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/Randomizer.kt
index 32e38aa4cb..85a536e81d 100644
--- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/Randomizer.kt
+++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/Randomizer.kt
@@ -18,7 +18,15 @@
package com.instructure.dataseeding.util
import com.github.javafaker.Faker
-import com.instructure.dataseeding.model.*
+import com.instructure.dataseeding.model.CreateAssignment
+import com.instructure.dataseeding.model.CreateDiscussionTopic
+import com.instructure.dataseeding.model.CreateGroup
+import com.instructure.dataseeding.model.CreateModule
+import com.instructure.dataseeding.model.CreateQuiz
+import com.instructure.dataseeding.model.CreateSubmissionComment
+import com.instructure.dataseeding.model.GradingType
+import com.instructure.dataseeding.model.SubmissionType
+import com.instructure.dataseeding.model.SubmitCourseAssignmentSubmission
import java.util.*
object Randomizer {
@@ -60,6 +68,10 @@ object Randomizer {
fun randomConversationSubject(): String = faker.chuckNorris().fact()
fun randomConversationBody(): String = faker.lorem().paragraph()
+ fun randomConferenceTitle(): String = faker.chuckNorris().fact()
+ fun randomConferenceDescription(): String = faker.lorem().paragraph()
+
+
fun randomEnrollmentTitle(): String = "${faker.pokemon()} Term"
fun randomGradingPeriodSetTitle(): String = "${faker.pokemon().location()} Set"
@@ -133,6 +145,8 @@ object Randomizer {
/** Creates a random Course Group Category name */
fun randomCourseGroupCategoryName(): String = faker.harryPotter().character()
+ fun randomModuleName(): String = faker.lordOfTheRings().location()
+
/** Creates random Group */
fun randomGroup() = CreateGroup(name = faker.harryPotter().location(), description = faker.harryPotter().quote())
diff --git a/automation/dataseedingapi/src/test/kotlin/com/instructure/dataseeding/soseedy/AssignmentsTest.kt b/automation/dataseedingapi/src/test/kotlin/com/instructure/dataseeding/soseedy/AssignmentsTest.kt
index 89eb012d4e..35d43b88d0 100644
--- a/automation/dataseedingapi/src/test/kotlin/com/instructure/dataseeding/soseedy/AssignmentsTest.kt
+++ b/automation/dataseedingapi/src/test/kotlin/com/instructure/dataseeding/soseedy/AssignmentsTest.kt
@@ -28,12 +28,12 @@ import com.instructure.dataseeding.api.UserApi
import com.instructure.dataseeding.model.AssignmentApiModel
import com.instructure.dataseeding.model.AssignmentOverrideApiModel
import com.instructure.dataseeding.model.AttachmentApiModel
+import com.instructure.dataseeding.model.FileType
+import com.instructure.dataseeding.model.GradingType.PERCENT
import com.instructure.dataseeding.model.SubmissionApiModel
import com.instructure.dataseeding.model.SubmissionCommentApiModel
-import com.instructure.dataseeding.model.FileType
-import com.instructure.dataseeding.model.SubmissionType.ONLINE_UPLOAD
import com.instructure.dataseeding.model.SubmissionType.ONLINE_TEXT_ENTRY
-import com.instructure.dataseeding.model.GradingType.PERCENT
+import com.instructure.dataseeding.model.SubmissionType.ONLINE_UPLOAD
import org.hamcrest.CoreMatchers.instanceOf
import org.junit.Assert.*
import org.junit.Before
@@ -112,7 +112,7 @@ class AssignmentsTest {
submissionTypes = listOf(ONLINE_TEXT_ENTRY),
teacherToken = teacher.token
))
- val submission = SubmissionsApi.commentOnSubmission(student.token,course.id,assignment.id,ArrayList())
+ val submission = SubmissionsApi.commentOnSubmission(course.id,student.token,assignment.id,ArrayList())
assertThat(submission, instanceOf(AssignmentApiModel::class.java))
assertEquals(1, submission.submissionComments?.size ?: 0)
val comment = submission.submissionComments?.get(0)
@@ -283,7 +283,7 @@ class AssignmentsTest {
studentToken = student.token,
submissionSeedsList = listOf(submissionSeed)
)
- val submissions = SubmissionsApi.seedAssignmentSubmission( request )
+ val submissions = SubmissionsApi.seedAssignmentSubmission( course.id, student.token, assignment.id, submissionSeedsList = listOf(submissionSeed))
if(submissions.isNotEmpty()) {
assertThat(submissions[0], instanceOf(SubmissionApiModel::class.java))
}
diff --git a/automation/dataseedingapi/src/test/kotlin/com/instructure/dataseeding/soseedy/ModulesTest.kt b/automation/dataseedingapi/src/test/kotlin/com/instructure/dataseeding/soseedy/ModulesTest.kt
index 02fd032053..31b0aeb4d6 100644
--- a/automation/dataseedingapi/src/test/kotlin/com/instructure/dataseeding/soseedy/ModulesTest.kt
+++ b/automation/dataseedingapi/src/test/kotlin/com/instructure/dataseeding/soseedy/ModulesTest.kt
@@ -6,7 +6,9 @@ import com.instructure.dataseeding.api.ModulesApi
import com.instructure.dataseeding.api.UserApi
import com.instructure.dataseeding.model.ModuleApiModel
import org.hamcrest.CoreMatchers.instanceOf
-import org.junit.Assert.*
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertThat
+import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@@ -57,7 +59,7 @@ class ModulesTest {
module = ModulesApi.updateModule(
courseId = course.id,
- id = module.id,
+ moduleId = module.id,
published = true,
teacherToken = teacher.token
)
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt
index adf8d1f501..f55dbbb6b8 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt
@@ -80,7 +80,7 @@ abstract class CanvasTest : InstructureTestingContract {
val connectivityManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
- @Rule(order = 1)
+ @Rule(order = 2)
override fun chain(): TestRule {
return RuleChain
.outerRule(ScreenshotTestRule())
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubCoverageAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubCoverageAnnotation.kt
new file mode 100644
index 0000000000..66d7c43636
--- /dev/null
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubCoverageAnnotation.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 - 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.canvas.espresso
+
+// When applied to a test method, denotes that the test is stubbed out from the coverage workflow.
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class StubCoverage(val description: String = "")
\ No newline at end of file
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt
index 3fb7dce239..045d36932b 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt
@@ -33,16 +33,16 @@ enum class Priority {
enum class FeatureCategory {
ASSIGNMENTS, SUBMISSIONS, LOGIN, COURSE, DASHBOARD, GROUPS, SETTINGS, PAGES, DISCUSSIONS, MODULES,
INBOX, GRADES, FILES, EVENTS, PEOPLE, CONFERENCES, COLLABORATIONS, SYLLABUS, TODOS, QUIZZES, NOTIFICATIONS,
- ANNOTATIONS, ANNOUNCEMENTS, COMMENTS, BOOKMARKS, NONE, K5_DASHBOARD, SPEED_GRADER, SYNC_SETTINGS, SYNC_PROGRESS, OFFLINE_CONTENT, LEFT_SIDE_MENU
+ ANNOTATIONS, ANNOUNCEMENTS, COMMENTS, BOOKMARKS, NONE, CANVAS_FOR_ELEMENTARY, SPEED_GRADER, SYNC_SETTINGS, SYNC_PROGRESS, OFFLINE_CONTENT, LEFT_SIDE_MENU
}
enum class SecondaryFeatureCategory {
NONE, LOGIN_K5,
SUBMISSIONS_TEXT_ENTRY, SUBMISSIONS_ANNOTATIONS, SUBMISSIONS_ONLINE_URL, SUBMISSIONS_MULTIPLE_TYPE,
- ASSIGNMENT_COMMENTS, ASSIGNMENT_QUIZZES, ASSIGNMENT_DISCUSSIONS,
+ ASSIGNMENT_COMMENTS, ASSIGNMENT_QUIZZES, ASSIGNMENT_DISCUSSIONS, HOMEROOM, K5_GRADES, IMPORTANT_DATES, RESOURCES, SCHEDULE,
GROUPS_DASHBOARD, GROUPS_FILES, GROUPS_ANNOUNCEMENTS, GROUPS_DISCUSSIONS, GROUPS_PAGES, GROUPS_PEOPLE,
EVENTS_DISCUSSIONS, EVENTS_QUIZZES, EVENTS_ASSIGNMENTS, EVENTS_NOTIFICATIONS,
- MODULES_ASSIGNMENTS, MODULES_DISCUSSIONS, MODULES_FILES, MODULES_PAGES, MODULES_QUIZZES, OFFLINE_MODE, ALL_COURSES, CHANGE_USER
+ MODULES_ASSIGNMENTS, MODULES_DISCUSSIONS, MODULES_FILES, MODULES_PAGES, MODULES_QUIZZES, OFFLINE_MODE, ALL_COURSES, CHANGE_USER, ASSIGNMENT_REMINDER
}
enum class TestCategory {
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt
index 2c81f9e357..687671757e 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt
@@ -880,7 +880,7 @@ fun MockCanvas.addAssignment(
lockInfo : LockInfo? = null,
userSubmitted: Boolean = false,
dueAt: String? = null,
- name: String = Randomizer.randomCourseName(),
+ name: String = Randomizer.randomAssignmentName(),
pointsPossible: Int = 10,
description: String = "",
lockAt: String? = null,
@@ -1338,7 +1338,8 @@ fun MockCanvas.addFileToFolder(
fileContent: String = Randomizer.randomPageBody(),
contentType: String = "text/plain",
url: String = "",
- fileId: Long = newItemId()
+ fileId: Long = newItemId(),
+ visibilityLevel: String = "inherit"
) : Long {
if(courseId == null && folderId == null) {
throw Exception("Either courseId or folderId must be non-null")
@@ -1352,7 +1353,8 @@ fun MockCanvas.addFileToFolder(
size = fileContent.length.toLong(),
displayName = displayName,
contentType = contentType,
- url = if(url.isEmpty()) "https://mock-data.instructure.com/files/$fileId/preview" else url
+ url = if(url.isEmpty()) "https://mock-data.instructure.com/files/$fileId/preview" else url,
+ visibilityLevel = visibilityLevel
)
// And record it for the folder
@@ -1368,8 +1370,6 @@ fun MockCanvas.addFileToFolder(
fileContents[fileId] = fileContent
return fileId
-
-
}
/**
* Creates a new file for the specified course, and records it properly in MockCanvas.
@@ -1378,12 +1378,13 @@ fun MockCanvas.addFileToFolder(
*/
fun MockCanvas.addFileToCourse(
courseId: Long,
- displayName: String,
+ displayName: String = Randomizer.randomPageTitle(),
fileContent: String = Randomizer.randomPageBody(),
contentType: String = "text/plain",
groupId: Long? = null,
url: String = "",
- fileId: Long = newItemId()
+ fileId: Long = newItemId(),
+ visibilityLevel: String = "inherit"
): Long {
val rootFolder = getRootFolder(courseId = courseId, groupId = groupId)
return addFileToFolder(
@@ -1392,7 +1393,8 @@ fun MockCanvas.addFileToCourse(
fileContent = fileContent,
contentType = contentType,
url = url,
- fileId = fileId
+ fileId = fileId,
+ visibilityLevel = visibilityLevel
)
}
@@ -1535,8 +1537,10 @@ fun MockCanvas.addItemToModule(
course: Course,
moduleId: Long,
item: Any,
+ contentId: Long = 0,
published: Boolean = true,
- moduleContentDetails: ModuleContentDetails? = null
+ moduleContentDetails: ModuleContentDetails? = null,
+ unpublishable: Boolean = true
) : ModuleItem {
// Placeholders for itemType and itemTitle values that we will compute below
@@ -1599,7 +1603,9 @@ fun MockCanvas.addItemToModule(
// htmlUrl populated in order to get external url module items to work.
url = itemUrl,
htmlUrl = itemUrl,
- moduleDetails = moduleContentDetails
+ contentId = contentId,
+ moduleDetails = moduleContentDetails,
+ unpublishable = unpublishable
)
// Copy/update/replace the module
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt
index 222d390b54..a3ed930581 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt
@@ -23,7 +23,9 @@ import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse
import com.instructure.canvas.espresso.mockCanvas.endpoint
import com.instructure.canvas.espresso.mockCanvas.utils.*
import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.utils.toDate
import okio.Buffer
+import retrofit2.http.GET
/**
* Base endpoint for the Canvas API
@@ -153,22 +155,82 @@ object ApiEndpoint : Endpoint(
) {
override val authModel = DontCareAuthModel
}
+ ),
+ Segment("progress") to Endpoint(
+ LongId(PathVars::progressId) to Endpoint {
+ GET {
+ request.successResponse(Progress(pathVars.progressId, workflowState = "completed"))
+ }
+ Segment("cancel") to Endpoint {
+ POST {
+ request.successResponse(Progress(pathVars.progressId, workflowState = "failed"))
+ }
+ }
+ }
)
)
object FileUrlEndpoint : Endpoint(
- LongId(PathVars::fileId) to endpoint(
- configure = {
- GET {
- val file = data.folderFiles.values.flatten().find { it.id == pathVars.fileId }
- if(file != null) {
- request.successResponse(file)
- } else {
- request.unauthorizedResponse()
+ LongId(PathVars::fileId) to Endpoint {
+ GET {
+ val file = data.folderFiles.values.flatten().find { it.id == pathVars.fileId }
+ if (file != null) {
+ request.successResponse(file)
+ } else {
+ request.unauthorizedResponse()
+ }
+ }
+ PUT {
+ val buffer = Buffer()
+ request.body!!.writeTo(buffer)
+ val body = buffer.readUtf8()
+ val updateFileFolder = Gson().fromJson(body, UpdateFileFolder::class.java)
+ val folderFiles = data.folderFiles.values.find { it.any { it.id == pathVars.fileId } }
+ val file = data.folderFiles.values.flatten().find { it.id == pathVars.fileId }
+ if (file == null || folderFiles == null) {
+ request.unauthorizedResponse()
+ } else {
+ val folderId = file.parentFolderId
+ val updatedFile = file.copy(
+ lockDate = updateFileFolder.lockAt.toDate() ?: file.lockDate,
+ unlockDate = updateFileFolder.unlockAt.toDate() ?: file.unlockDate,
+ isLocked = updateFileFolder.locked ?: file.isLocked,
+ isHidden = updateFileFolder.hidden ?: file.isHidden,
+ visibilityLevel = updateFileFolder.visibilityLevel ?: file.visibilityLevel
+ )
+ val updatedFolderFiles = folderFiles.map { if (it.id == pathVars.fileId) updatedFile else it }
+ data.folderFiles[folderId] = updatedFolderFiles.toMutableList()
+
+ val affectedCourseMap = data.courseModules.filterValues { modules ->
+ modules.any { module ->
+ module.items.any { it.contentId == pathVars.fileId }
+ }
+ }
+
+ affectedCourseMap.forEach { (courseId, modules) ->
+ val updatedModules = modules.map { module ->
+ val updatedItems = module.items.filter { it.contentId == pathVars.fileId }
+ .associate { moduleItem ->
+ moduleItem.id to moduleItem.copy(
+ published = !updatedFile.isLocked,
+ moduleDetails = ModuleContentDetails(
+ lockAt = updatedFile.lockDate?.toString(),
+ unlockAt = updatedFile.unlockDate?.toString(),
+ locked = updatedFile.isLocked,
+ hidden = updatedFile.isHidden
+ )
+ )
+ }
+ module.copy(items = module.items.map { moduleItem -> updatedItems[moduleItem.id] ?: moduleItem })
+ }
+ data.courseModules[courseId] = updatedModules.toMutableList()
}
+
+ request.successResponse(updatedFile)
}
+
}
- )
+ }
)
object CanvadocRedirectEndpoint : Endpoint(
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt
index a9a0ed63f7..e8a662d622 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt
@@ -21,6 +21,8 @@ import com.google.gson.Gson
import com.instructure.canvas.espresso.mockCanvas.*
import com.instructure.canvas.espresso.mockCanvas.utils.*
import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.postmodels.BulkUpdateProgress
+import com.instructure.canvasapi2.models.postmodels.BulkUpdateResponse
import com.instructure.canvasapi2.models.postmodels.UpdateCourseWrapper
import com.instructure.canvasapi2.utils.globalName
import com.instructure.canvasapi2.utils.toApiString
@@ -817,6 +819,35 @@ object CourseModuleListEndpoint : Endpoint(
request.unauthorizedResponse()
}
}
+ PUT {
+ val moduleIds = request.url.queryParameterValues("module_ids[]").filterNotNull().map { it.toLong() }
+ val event = request.url.queryParameter("event")
+ val skipContentTags = request.url.queryParameter("skip_content_tags").toBoolean()
+
+ val modules = data.courseModules[pathVars.courseId]?.filter { moduleIds.contains(it.id) }
+
+ val updatedModules = modules?.map {
+ val updatedItems = if (skipContentTags) {
+ it.items
+ } else {
+ it.items.map { it.copy(published = event == "publish") }
+ }
+ it.copy(
+ published = event == "publish",
+ items = updatedItems
+ )
+ }
+
+ data.courseModules[pathVars.courseId]?.map { moduleObject ->
+ updatedModules?.find { updatedModuleObject ->
+ updatedModuleObject.id == moduleObject.id
+ } ?: moduleObject
+ }?.let {
+ data.courseModules[pathVars.courseId] = it.toMutableList()
+ }
+
+ request.successResponse(BulkUpdateResponse(BulkUpdateProgress(Progress(1L, workflowState = "running"))))
+ }
}
)
@@ -865,7 +896,42 @@ object CourseModuleItemsListEndpoint : Endpoint(
} else {
request.unauthorizedResponse()
}
+ }
+ PUT {
+ val isPublished = request.url.queryParameter("module_item[published]").toBoolean()
+ val moduleList = data.courseModules[pathVars.courseId]
+ val moduleObject = moduleList?.find { it.id == pathVars.moduleId }
+ val itemList = moduleObject?.items
+ val moduleItem = itemList?.find { it.id == pathVars.moduleItemId }
+
+ if (moduleItem != null) {
+ val updatedItem = moduleItem.copy(published = isPublished)
+
+ val updatedModule = moduleObject.copy(
+ items = itemList.map {
+ if (it.id == updatedItem.id) {
+ updatedItem
+ } else {
+ it
+ }
+ }
+ )
+
+ data.courseModules[pathVars.courseId]?.map {
+ if (it.id == updatedModule.id) {
+ updatedModule
+ } else {
+ it
+ }
+ }?.let {
+ data.courseModules[pathVars.courseId] = it.toMutableList()
+ }
+
+ request.successResponse(updatedItem)
+ } else {
+ request.unauthorizedResponse()
+ }
}
},
response = {
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt
index ff8beac66c..a46d2247bf 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt
@@ -48,6 +48,7 @@ class PathVars {
var annotationid: String by map
var bookmarkId: Long by map
var enrollmentId: Long by map
+ var progressId: Long by map
}
/**
diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/ModuleItemInteractions.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/ModuleItemInteractions.kt
new file mode 100644
index 0000000000..e32d598311
--- /dev/null
+++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/ModuleItemInteractions.kt
@@ -0,0 +1,80 @@
+package com.instructure.espresso
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.instructure.espresso.page.plus
+
+class ModuleItemInteractions(private val moduleNameId: Int? = null, private val nextArrowId: Int? = null, private val previousArrowId: Int? = null) {
+
+ /**
+ * Assert module name displayed
+ *
+ * @param moduleName
+ */
+ fun assertModuleNameDisplayed(moduleName: String) {
+ onView(moduleNameId?.let { withId(it) + ViewMatchers.withText(moduleName)}).assertDisplayed()
+ }
+
+
+ /**
+ * Click on next arrow to navigate to the next module item's details
+ *
+ */
+ fun clickOnNextArrow() {
+ onView(nextArrowId?.let { withId(it) }).click()
+ }
+
+ /**
+ * Click on previous arrow to navigate to the previous module item's details
+ *
+ */
+ fun clickOnPreviousArrow() {
+ onView(previousArrowId?.let { withId(it) }).click()
+ }
+
+ /**
+ * Assert previous arrow not displayed (e.g. invisible)
+ *
+ */
+ fun assertPreviousArrowNotDisplayed() {
+ onView(previousArrowId?.let { withId(it) }).check(
+ ViewAssertions.matches(
+ ViewMatchers.withEffectiveVisibility(
+ ViewMatchers.Visibility.INVISIBLE)))
+ }
+
+ /**
+ * Assert previous arrow displayed
+ *
+ */
+ fun assertPreviousArrowDisplayed() {
+ onView(previousArrowId?.let { withId(it) }).check(
+ ViewAssertions.matches(
+ ViewMatchers.withEffectiveVisibility(
+ ViewMatchers.Visibility.VISIBLE)))
+ }
+
+ /**
+ * Assert next arrow displayed
+ *
+ */
+ fun assertNextArrowDisplayed() {
+ onView(nextArrowId?.let { withId(it) }).check(
+ ViewAssertions.matches(
+ ViewMatchers.withEffectiveVisibility(
+ ViewMatchers.Visibility.VISIBLE)))
+ }
+
+ /**
+ * Assert next arrow not displayed (e.g. invisible)
+ *
+ */
+ fun assertNextArrowNotDisplayed() {
+ onView(nextArrowId?.let { withId(it) }).check(
+ ViewAssertions.matches(
+ ViewMatchers.withEffectiveVisibility(
+ ViewMatchers.Visibility.INVISIBLE)))
+ }
+}
\ No newline at end of file
diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestRail.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestRail.kt
deleted file mode 100644
index d00d894208..0000000000
--- a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestRail.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.instructure.espresso
-
-import java.lang.annotation.Retention
-import java.lang.annotation.RetentionPolicy
-
-@Retention(RetentionPolicy.RUNTIME)
-@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
-annotation class TestRail(val ID: String = "unknown")
\ No newline at end of file
diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt
index 2901820aae..42fe0e5770 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt
@@ -91,4 +91,10 @@ fun retryWithIncreasingDelay(
}
}
block()
+}
+
+fun extractInnerTextById(html: String, id: String): String? {
+ val pattern = "<[^>]*?\\bid=\"$id\"[^>]*?>(.*?)[^>]*?>".toRegex(RegexOption.DOT_MATCHES_ALL)
+ val matchResult = pattern.find(html)
+ return matchResult?.groupValues?.getOrNull(1)
}
\ No newline at end of file
diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/ViewInteractionExtensions.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/ViewInteractionExtensions.kt
index 1e4c2d3ac8..f6e0332e27 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/espresso/ViewInteractionExtensions.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/ViewInteractionExtensions.kt
@@ -154,3 +154,19 @@ fun ViewInteraction.waitForCheck(assertion: ViewAssertion) {
} while (System.currentTimeMillis() < endTime)
check(assertion)
}
+
+fun ViewInteraction.assertChecked() {
+ check(ViewAssertions.matches(ViewMatchers.isChecked()))
+}
+
+fun ViewInteraction.assertNotChecked() {
+ check(ViewAssertions.matches(ViewMatchers.isNotChecked()))
+}
+
+fun ViewInteraction.assertEnabled() {
+ check(ViewAssertions.matches(ViewMatchers.isEnabled()))
+}
+
+fun ViewInteraction.assertDisabled() {
+ check(ViewAssertions.matches(Matchers.not(ViewMatchers.isEnabled())))
+}
\ No newline at end of file
diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/page/PageExtensions.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/page/PageExtensions.kt
index cae3a84948..87860ec791 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/espresso/page/PageExtensions.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/page/PageExtensions.kt
@@ -18,6 +18,7 @@
package com.instructure.espresso.page
import android.view.View
+import androidx.annotation.PluralsRes
import androidx.test.espresso.Espresso
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.matcher.ViewMatchers
@@ -160,6 +161,11 @@ fun BasePage.getStringFromResource(stringResource: Int, vararg params: Any): Str
return targetContext.resources.getString(stringResource, *params)
}
+fun BasePage.getPluralFromResource(@PluralsRes pluralsResource: Int, quantity: Int, vararg params: Any): String {
+ val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
+ return targetContext.resources.getQuantityString(pluralsResource, quantity, *params)
+}
+
fun BasePage.callOnClick(matcher: Matcher) = ViewCallOnClick.callOnClick(matcher)
fun BasePage.scrollTo(viewId: Int) = BaristaScrollInteractions.safelyScrollTo(viewId)
diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt
index 287dfe6e02..7b9cbc0fe9 100644
--- a/buildSrc/src/main/java/GlobalDependencies.kt
+++ b/buildSrc/src/main/java/GlobalDependencies.kt
@@ -18,6 +18,7 @@ object Versions {
/* Kotlin */
const val KOTLIN = "1.9.20"
const val KOTLIN_COROUTINES = "1.6.4"
+ const val KOTLIN_COMPOSE_COMPILER_VERSION = "1.5.4"
/* Google, Play Services */
const val GOOGLE_SERVICES = "4.3.15"
@@ -161,6 +162,16 @@ object Libs {
const val ROOM_TEST = "androidx.room:room-testing:${Versions.ROOM}"
const val HAMCREST = "org.hamcrest:hamcrest:${Versions.HAMCREST}"
+
+ // Compose
+ const val COMPOSE_BOM = "androidx.compose:compose-bom:2023.10.01"
+ const val COMPOSE_MATERIAL = "androidx.compose.material:material"
+ const val COMPOSE_PREVIEW = "androidx.compose.ui:ui-tooling-preview"
+ const val COMPOSE_TOOLING = "androidx.compose.ui:ui-tooling"
+ const val COMPOSE_UI = "androidx.compose.ui:ui-android"
+ const val COMPOSE_VIEW_MODEL = "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1"
+ const val COMPOSE_UI_TEST = "androidx.compose.ui:ui-test-junit4:1.5.4"
+ const val COMPOSE_UI_TEST_MANIFEST = "androidx.compose.ui:ui-test-manifest"
}
object Plugins {
diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt
index cf85789cdc..fe61a10dd4 100644
--- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt
+++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt
@@ -98,6 +98,9 @@ object FileFolderAPI {
@PUT("files/{fileId}?include[]=usage_rights")
fun updateFile(@Path("fileId") fileId: Long, @Body updateFileFolder: UpdateFileFolder): Call
+ @PUT("files/{fileId}?include[]=usage_rights")
+ suspend fun updateFile(@Path("fileId") fileId: Long, @Body updateFileFolder: UpdateFileFolder, @Tag params: RestParams): DataResult
+
@POST("folders/{folderId}/folders")
fun createFolder(@Path("folderId") folderId: Long, @Body newFolderName: CreateFolder): Call
@@ -119,6 +122,9 @@ object FileFolderAPI {
@GET("files/{fileNumber}?include=avatar")
fun getAvatarFileToken(@Path("fileNumber") fileNumber: String): Call
+
+ @GET("files/{fileId}")
+ suspend fun getFile(@Path("fileId") fileId: Long, @Tag params: RestParams): DataResult
}
fun getFileFolderFromURL(adapter: RestBuilder, url: String, callback: StatusCallback, params: RestParams) {
diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ModuleAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ModuleAPI.kt
index 0b7553d3da..04f55a7d98 100644
--- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ModuleAPI.kt
+++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ModuleAPI.kt
@@ -20,6 +20,7 @@ import com.instructure.canvasapi2.StatusCallback
import com.instructure.canvasapi2.builders.RestBuilder
import com.instructure.canvasapi2.builders.RestParams
import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.postmodels.BulkUpdateResponse
import com.instructure.canvasapi2.utils.DataResult
import okhttp3.ResponseBody
import retrofit2.Call
@@ -81,6 +82,15 @@ object ModuleAPI {
@GET("{contextId}/modules/{moduleId}/items/{itemId}?include[]=content_details")
fun getModuleItem(@Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Path("itemId") itemId: Long) : Call
+
+ @GET("{contextType}/{contextId}/modules/{moduleId}/items/{itemId}?include[]=content_details")
+ suspend fun getModuleItem(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Path("itemId") itemId: Long, @Tag params: RestParams) : DataResult
+
+ @PUT("{contextType}/{contextId}/modules")
+ suspend fun bulkUpdateModules(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Query("module_ids[]") moduleIds: List, @Query("event") event: String, @Query("skip_content_tags") skipContentTags: Boolean, @Query("async") async: Boolean, @Tag params: RestParams): DataResult
+
+ @PUT("{contextType}/{contextId}/modules/{moduleId}/items/{itemId}")
+ suspend fun publishModuleItem(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Path("itemId") itemId: Long, @Query("module_item[published]") publish: Boolean, @Tag params: RestParams): DataResult
}
diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ProgressAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ProgressAPI.kt
index 6223cc1541..137d1b463b 100644
--- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ProgressAPI.kt
+++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ProgressAPI.kt
@@ -32,6 +32,9 @@ object ProgressAPI {
@GET("progress/{progressId}")
suspend fun getProgress(@Path("progressId") progressId: String, @Tag params: RestParams): DataResult