diff --git a/android-vault b/android-vault
index ddd561a8ef..15225ede6c 160000
--- a/android-vault
+++ b/android-vault
@@ -1 +1 @@
-Subproject commit ddd561a8ef8289f2ed248925679827b2da5ab016
+Subproject commit 15225ede6c44da8265e5cdaea34d49dbc47cb8f5
diff --git a/apps/flutter_parent/android/app/build.gradle b/apps/flutter_parent/android/app/build.gradle
index 823d43f00f..5fd86f30e5 100644
--- a/apps/flutter_parent/android/app/build.gradle
+++ b/apps/flutter_parent/android/app/build.gradle
@@ -104,7 +104,7 @@ dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
- androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation "com.squareup.okhttp3:okhttp:4.9.1"
implementation 'org.jsoup:jsoup:1.11.3'
implementation 'com.google.gms:google-services:4.3.14'
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/l10n/app_localizations.dart b/apps/flutter_parent/lib/l10n/app_localizations.dart
index 53eda6d918..86768767b3 100644
--- a/apps/flutter_parent/lib/l10n/app_localizations.dart
+++ b/apps/flutter_parent/lib/l10n/app_localizations.dart
@@ -989,11 +989,18 @@ class AppLocalizations {
desc: 'Screen reader-friendly replacement for the "-" character in letter grades like "A-"',
);
- String latePenalty(String pointsLost) => Intl.message(
- 'Late penalty (-$pointsLost)',
+ String yourGrade(String pointsAchieved) => Intl.message(
+ 'Your grade: $pointsAchieved',
+ desc: 'Text displayed when a late penalty has been applied to the assignment, this is the achieved score without the penalty',
+ args: [pointsAchieved],
+ name: 'yourGrade',
+ );
+
+ String latePenaltyUpdated(String pointsLost) => Intl.message(
+ 'Late Penalty: -$pointsLost pts',
desc: 'Text displayed when a late penalty has been applied to the assignment',
args: [pointsLost],
- name: 'latePenalty',
+ name: 'latePenaltyUpdated',
);
String finalGrade(String grade) => Intl.message(
diff --git a/apps/flutter_parent/lib/l10n/res/intl_id.arb b/apps/flutter_parent/lib/l10n/res/intl_id.arb
index 8d63ed0338..5975fdcc49 100644
--- a/apps/flutter_parent/lib/l10n/res/intl_id.arb
+++ b/apps/flutter_parent/lib/l10n/res/intl_id.arb
@@ -1,5 +1,5 @@
{
- "@@last_modified": "2022-10-28T11:03:07.232972",
+ "@@last_modified": "2023-08-25T11:04:20.901151",
"alertsLabel": "Peringatan",
"@alertsLabel": {
"description": "The label for the Alerts tab",
@@ -182,6 +182,19 @@
"points": {}
}
},
+ "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} acara}other{{date}, {eventCount} acara}}",
+ "@calendarDaySemanticsLabel": {
+ "description": "Screen reader label used for calendar day, reads the date and count of events",
+ "type": "text",
+ "placeholders_order": [
+ "date",
+ "eventCount"
+ ],
+ "placeholders": {
+ "date": {},
+ "eventCount": {}
+ }
+ },
"No Events Today!": "Tidak Ada Acara Hari Ini!",
"@No Events Today!": {
"description": "Title displayed when there are no calendar events for the current day",
@@ -2666,5 +2679,75 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
+ },
+ "Acceptable Use Policy": "Kebijakan Penggunaan yang Dapat Diterima",
+ "@Acceptable Use Policy": {
+ "description": "title for the acceptable use policy screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Submit": "Serahkan",
+ "@Submit": {
+ "description": "submit button title for acceptable use policy screen",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Either you're a new user or the Acceptable Use Policy has changed since you last agreed to it. Please agree to the Acceptable Use Policy before you continue.": "Anda pengguna baru atau Kebijakan Penggunaan yang Dapat Diterima telah berubah sejak Anda terakhir kali menyetujuinya. Silakan setujui Kebijakan Penggunaan yang Dapat Diterima sebelum Anda melanjutkan.",
+ "@Either you're a new user or the Acceptable Use Policy has changed since you last agreed to it. Please agree to the Acceptable Use Policy before you continue.": {
+ "description": "acceptable use policy screen description",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "I agree to the Acceptable Use Policy.": "Saya menyetujui Kebijakan Penggunaan yang Dapat Diterima.",
+ "@I agree to the Acceptable Use Policy.": {
+ "description": "acceptable use policy switch title",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "About": "Tentang",
+ "@About": {
+ "description": "Title for about menu item in settings",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "App": "App",
+ "@App": {
+ "description": "Title for App field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Login ID": "ID Login",
+ "@Login ID": {
+ "description": "Title for Login ID field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Email": "Email",
+ "@Email": {
+ "description": "Title for Email field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Version": "Versi",
+ "@Version": {
+ "description": "Title for Version field on about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "Instructure logo": "Logo Instructure",
+ "@Instructure logo": {
+ "description": "Semantics label for the Instructure logo on the about page",
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
}
}
\ No newline at end of file
diff --git a/apps/flutter_parent/lib/l10n/res/intl_ja.arb b/apps/flutter_parent/lib/l10n/res/intl_ja.arb
index 4a6e294d49..cf1ca754a3 100644
--- a/apps/flutter_parent/lib/l10n/res/intl_ja.arb
+++ b/apps/flutter_parent/lib/l10n/res/intl_ja.arb
@@ -171,7 +171,7 @@
"placeholders_order": [],
"placeholders": {}
},
- "pointsPossible": "{points}の可能なポイント",
+ "pointsPossible": "配点 {points}",
"@pointsPossible": {
"description": "Screen reader label used for the points possible for an assignment, quiz, etc.",
"type": "text",
diff --git a/apps/flutter_parent/lib/models/assignment.dart b/apps/flutter_parent/lib/models/assignment.dart
index 6175b081eb..3947713bdf 100644
--- a/apps/flutter_parent/lib/models/assignment.dart
+++ b/apps/flutter_parent/lib/models/assignment.dart
@@ -145,7 +145,7 @@ abstract class Assignment implements Built {
SubmissionStatus getStatus({required String? studentId}) {
final submission = this.submission(studentId);
- if (!isSubmittable()) {
+ if (!isSubmittable() && submission == null) {
return SubmissionStatus.NONE;
} else if (submission?.isLate == true) {
return SubmissionStatus.LATE;
diff --git a/apps/flutter_parent/lib/models/grade_cell_data.dart b/apps/flutter_parent/lib/models/grade_cell_data.dart
index 642c29700e..f30b60241f 100644
--- a/apps/flutter_parent/lib/models/grade_cell_data.dart
+++ b/apps/flutter_parent/lib/models/grade_cell_data.dart
@@ -40,6 +40,7 @@ abstract class GradeCellData implements Built 0.0) {
grade = ''; // Grade will be shown in the 'final grade' text
var pointsDeducted = NumberFormat.decimalPattern().format(submission.pointsDeducted ?? 0.0);
- latePenalty = l10n.latePenalty(pointsDeducted);
+ var pointsAchieved = NumberFormat.decimalPattern().format(submission.enteredScore);
+ yourGrade = l10n.yourGrade(pointsAchieved);
+ latePenalty = l10n.latePenaltyUpdated(pointsDeducted);
finalGrade = l10n.finalGrade(submission.grade ?? grade);
}
@@ -166,6 +171,7 @@ abstract class GradeCellData implements Built _$this._outOf;
set outOf(String? outOf) => _$this._outOf = outOf;
+ String? _yourGrade;
+ String? get yourGrade => _$this._yourGrade;
+ set yourGrade(String? yourGrade) => _$this._yourGrade = yourGrade;
+
String? _latePenalty;
String? get latePenalty => _$this._latePenalty;
set latePenalty(String? latePenalty) => _$this._latePenalty = latePenalty;
@@ -221,6 +233,7 @@ class GradeCellDataBuilder
_grade = $v.grade;
_gradeContentDescription = $v.gradeContentDescription;
_outOf = $v.outOf;
+ _yourGrade = $v.yourGrade;
_latePenalty = $v.latePenalty;
_finalGrade = $v.finalGrade;
_$v = null;
@@ -264,6 +277,7 @@ class GradeCellDataBuilder
grade: BuiltValueNullFieldError.checkNotNull(grade, r'GradeCellData', 'grade'),
gradeContentDescription: BuiltValueNullFieldError.checkNotNull(gradeContentDescription, r'GradeCellData', 'gradeContentDescription'),
outOf: BuiltValueNullFieldError.checkNotNull(outOf, r'GradeCellData', 'outOf'),
+ yourGrade: BuiltValueNullFieldError.checkNotNull(yourGrade, r'GradeCellData', 'yourGrade'),
latePenalty: BuiltValueNullFieldError.checkNotNull(latePenalty, r'GradeCellData', 'latePenalty'),
finalGrade: BuiltValueNullFieldError.checkNotNull(finalGrade, r'GradeCellData', 'finalGrade'));
replace(_$result);
diff --git a/apps/flutter_parent/lib/screens/account_creation/account_creation_interactor.dart b/apps/flutter_parent/lib/screens/account_creation/account_creation_interactor.dart
index 584172d22a..e042eeba79 100644
--- a/apps/flutter_parent/lib/screens/account_creation/account_creation_interactor.dart
+++ b/apps/flutter_parent/lib/screens/account_creation/account_creation_interactor.dart
@@ -35,6 +35,6 @@ class AccountCreationInteractor {
}
launchPrivacyPolicy() {
- locator().launch('https://www.instructure.com/canvas/privacy');
+ locator().launch('https://www.instructure.com/policies/product-privacy-policy');
}
}
diff --git a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart
index 9043221d75..2814bad688 100644
--- a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart
+++ b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart
@@ -142,7 +142,8 @@ class _AssignmentDetailsScreenState extends State {
final assignment = snapshot.data!.assignment!;
final submission = assignment.submission(_currentStudent?.id);
final fullyLocked = assignment.isFullyLocked;
- final showStatus = assignment.isSubmittable() || submission?.isGraded() == true;
+ final missing = submission?.missing == true;
+ final showStatus = missing || assignment.isSubmittable() || submission?.isGraded() == true;
final submitted = submission?.submittedAt != null;
final submittedColor = submitted ? ParentTheme.of(context)?.successColor : textTheme.bodySmall?.color;
@@ -172,9 +173,9 @@ class _AssignmentDetailsScreenState extends State {
if (showStatus) SizedBox(width: 8),
if (showStatus)
Text(
- !submitted
- ? l10n.assignmentNotSubmittedLabel
- : submission?.isGraded() == true ? l10n.assignmentGradedLabel : l10n.assignmentSubmittedLabel,
+ missing ? l10n.assignmentMissingSubmittedLabel :
+ !submitted ? l10n.assignmentNotSubmittedLabel :
+ submission?.isGraded() == true ? l10n.assignmentGradedLabel : l10n.assignmentSubmittedLabel,
style: textTheme.bodySmall?.copyWith(
color: submittedColor,
),
diff --git a/apps/flutter_parent/lib/screens/assignments/grade_cell.dart b/apps/flutter_parent/lib/screens/assignments/grade_cell.dart
index 9d8bb559bf..d8bac77e11 100644
--- a/apps/flutter_parent/lib/screens/assignments/grade_cell.dart
+++ b/apps/flutter_parent/lib/screens/assignments/grade_cell.dart
@@ -18,6 +18,7 @@ import 'package:flutter_parent/models/assignment.dart';
import 'package:flutter_parent/models/course.dart';
import 'package:flutter_parent/models/grade_cell_data.dart';
import 'package:flutter_parent/models/submission.dart';
+import 'package:flutter_parent/utils/design/parent_colors.dart';
import 'package:flutter_parent/utils/design/parent_theme.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
@@ -134,37 +135,49 @@ class GradeCell extends StatelessWidget {
],
),
if (!_isGradedRestrictQuantitativeData) SizedBox(width: 16),
- if (!_isGradedRestrictQuantitativeData) Expanded(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (data.grade.isNotEmpty)
- Text(
- data.grade,
- key: Key('grade-cell-grade'),
- style: Theme.of(context).textTheme.headlineMedium,
- semanticsLabel: data.gradeContentDescription,
- ),
- if (data.outOf.isNotEmpty) Text(data.outOf, key: Key('grade-cell-out-of')),
- if (data.latePenalty.isNotEmpty)
- Text(
- data.latePenalty,
- style: TextStyle(color: data.accentColor),
- key: Key('grade-cell-late-penalty'),
- ),
- if (data.finalGrade.isNotEmpty)
- Padding(
- padding: const EdgeInsets.only(top: 8),
- child: Text(
- data.finalGrade,
- key: Key('grade-cell-final-grade'),
- style: Theme.of(context).textTheme.titleMedium,
+ if (!_isGradedRestrictQuantitativeData)
+ Expanded(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (data.grade.isNotEmpty)
+ Text(
+ data.grade,
+ key: Key('grade-cell-grade'),
+ style: Theme.of(context).textTheme.headlineMedium,
+ semanticsLabel: data.gradeContentDescription,
+ ),
+ if (data.outOf.isNotEmpty) Text(data.outOf, key: Key('grade-cell-out-of')),
+ if (data.yourGrade.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.only(top: 4),
+ child: Text(
+ data.yourGrade,
+ key: Key('grade-cell-your-grade'),
+ ),
+ ),
+ if (data.latePenalty.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.only(top: 4),
+ child: Text(
+ data.latePenalty,
+ style: TextStyle(color: ParentColors.failure),
+ key: Key('grade-cell-late-penalty'),
+ ),
+ ),
+ if (data.finalGrade.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.only(top: 4),
+ child: Text(
+ data.finalGrade,
+ key: Key('grade-cell-final-grade'),
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
),
- ),
- ],
+ ],
+ ),
),
- ),
],
);
}
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/lib/screens/settings/legal_screen.dart b/apps/flutter_parent/lib/screens/settings/legal_screen.dart
index c6b56ff3cd..90323f5401 100644
--- a/apps/flutter_parent/lib/screens/settings/legal_screen.dart
+++ b/apps/flutter_parent/lib/screens/settings/legal_screen.dart
@@ -34,7 +34,7 @@ class LegalScreen extends StatelessWidget {
children: [
_LegalRow(
label: l10n.privacyPolicy,
- onTap: () => locator().launch('https://www.instructure.com/canvas/privacy'),
+ onTap: () => locator().launch('https://www.instructure.com/policies/product-privacy-policy'),
icon: CanvasIcons.admin,
),
_LegalRow(
diff --git a/apps/flutter_parent/lib/utils/alert_helper.dart b/apps/flutter_parent/lib/utils/alert_helper.dart
index c4403e4b95..236a005483 100644
--- a/apps/flutter_parent/lib/utils/alert_helper.dart
+++ b/apps/flutter_parent/lib/utils/alert_helper.dart
@@ -27,7 +27,7 @@ class AlertsHelper {
filteredList.add(element);
} else {
Course? course = await locator().getCourse(courseId, forceRefresh: false);
- if (course?.settings?.restrictQuantitativeData == false) {
+ if (!(course?.settings?.restrictQuantitativeData ?? false)) {
filteredList.add(element);
}
}
diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml
index 013bf5be43..0c370d64e5 100644
--- a/apps/flutter_parent/pubspec.yaml
+++ b/apps/flutter_parent/pubspec.yaml
@@ -25,7 +25,7 @@ description: Canvas Parent
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 3.9.1+49
+version: 3.10.0+50
module:
androidX: true
diff --git a/apps/flutter_parent/test/screens/account_creation/account_creation_interactor_test.dart b/apps/flutter_parent/test/screens/account_creation/account_creation_interactor_test.dart
index 600529acca..0569a4a601 100644
--- a/apps/flutter_parent/test/screens/account_creation/account_creation_interactor_test.dart
+++ b/apps/flutter_parent/test/screens/account_creation/account_creation_interactor_test.dart
@@ -57,7 +57,7 @@ void main() {
test('launchPrivacyPolicy calls the url launcher', () {
AccountCreationInteractor().launchPrivacyPolicy();
verify(
- launcher.launch('https://www.instructure.com/canvas/privacy'),
+ launcher.launch('https://www.instructure.com/policies/product-privacy-policy'),
).called(1);
});
}
diff --git a/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart b/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart
index 134975c7df..2fe2782299 100644
--- a/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart
+++ b/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart
@@ -250,10 +250,11 @@ void main() {
..grade = '79'
..score = 79.0);
var expected = baseGradedState.rebuild((b) => b
- ..graphPercent = 0.85
- ..score = '85'
+ ..graphPercent = 0.79
+ ..score = '79'
..showPointsLabel = true
- ..latePenalty = 'Late penalty (-6)'
+ ..yourGrade = 'Your grade: 85'
+ ..latePenalty = 'Late Penalty: -6 pts'
..finalGrade = 'Final Grade: 79');
var actual = GradeCellData.forSubmission(baseCourse, baseAssignment, submission, theme, l10n);
expect(actual, expected);
diff --git a/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart b/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart
index 37bd688ec3..3cc7c10885 100644
--- a/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart
+++ b/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart
@@ -16,6 +16,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_parent/l10n/app_localizations.dart';
import 'package:flutter_parent/models/grade_cell_data.dart';
import 'package:flutter_parent/screens/assignments/grade_cell.dart';
+import 'package:flutter_parent/utils/design/parent_colors.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
@@ -158,11 +159,13 @@ void main() {
GradeCellData data = GradeCellData((b) => b
..state = GradeCellState.graded
..accentColor = Colors.pinkAccent
- ..latePenalty = 'Late penalty: (-25)');
+ ..yourGrade = 'Your grade: 85'
+ ..latePenalty = 'Late penalty: -25');
await setupWithData(tester, data);
expect(find.byKey(Key('grade-cell-late-penalty')).evaluate(), find.text(data.latePenalty).evaluate());
- expect(tester.widget(find.byKey(Key('grade-cell-late-penalty'))).style!.color, data.accentColor);
+ expect(find.byKey(Key('grade-cell-your-grade')).evaluate(), find.text(data.yourGrade).evaluate());
+ expect(tester.widget(find.byKey(Key('grade-cell-late-penalty'))).style!.color, ParentColors.failure);
});
testWidgetsWithAccessibilityChecks('Displays final grade text', (tester) async {
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/flutter_parent/test/screens/settings/legal_screen_test.dart b/apps/flutter_parent/test/screens/settings/legal_screen_test.dart
index 6055ed0335..bd6ce8d482 100644
--- a/apps/flutter_parent/test/screens/settings/legal_screen_test.dart
+++ b/apps/flutter_parent/test/screens/settings/legal_screen_test.dart
@@ -47,7 +47,7 @@ void main() {
await tester.tap(find.text(l10n.privacyPolicy));
await tester.pumpAndSettle();
- verify(mockLauncher.launch('https://www.instructure.com/canvas/privacy')).called(1);
+ verify(mockLauncher.launch('https://www.instructure.com/policies/product-privacy-policy')).called(1);
});
testWidgetsWithAccessibilityChecks('tapping github launches url', (tester) async {
diff --git a/apps/settings.gradle b/apps/settings.gradle
index 011f248a08..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]))
@@ -22,8 +35,6 @@ include ':pandautils'
include ':rceditor'
include ':recyclerview'
include ':pandares'
-include ':panda_annotations'
-include ':panda_processor'
include ':DocumentScanner'
project(':annotations').projectDir = new File(rootProject.projectDir, '/../libs/annotations')
@@ -38,6 +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(':panda_annotations').projectDir = new File(rootProject.projectDir, '/../libs/panda_annotations')
-project(':panda_processor').projectDir = new File(rootProject.projectDir, '/../libs/panda_processor')
-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 eb671160b8..815299e864 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 = 255
- versionName = '6.26.1'
+ versionCode = 260
+ versionName = '7.2.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,12 +248,11 @@ android {
hilt {
enableTransformForLocalTests = true
enableAggregatingTask = false
+ enableExperimentalClasspathAggregation = true
}
}
dependencies {
- implementation project(path: ':panda_annotations')
- kaptAndroidTestQa project(path: ':panda_processor')
implementation fileTree(dir: 'libs', include: ['*.jar'])
@@ -320,7 +327,8 @@ dependencies {
implementation Libs.SQLDELIGHT
/* Qr Code */
- implementation Libs.JOURNEY_ZXING
+ implementation (Libs.JOURNEY_ZXING) { transitive = false }
+ implementation Libs.JOURNEY_ZXING_CORE
/* AAC */
implementation Libs.VIEW_MODEL
@@ -349,6 +357,10 @@ dependencies {
implementation Libs.ROOM
kapt Libs.ROOM_COMPILER
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/espresso/TestAppManager.kt b/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt
index c99b8cf92d..fc00cc637e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt
@@ -16,7 +16,6 @@
*/
package com.instructure.student.espresso
-import androidx.work.Configuration
import androidx.work.WorkerFactory
import com.instructure.student.util.BaseAppManager
@@ -25,8 +24,6 @@ open class TestAppManager : BaseAppManager() {
var workerFactory: WorkerFactory? = null
override fun getWorkManagerFactory(): WorkerFactory {
- return workerFactory?.let {
- it
- } ?: WorkerFactory.getDefaultWorkerFactory()
+ return workerFactory ?: WorkerFactory.getDefaultWorkerFactory()
}
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt
index 755499d291..29646fe3f9 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt
@@ -19,13 +19,13 @@ 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.canvasapi2.models.DiscussionEntry
import com.instructure.dataseeding.api.DiscussionTopicsApi
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
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/e2e/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt
index f546bc83ea..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
@@ -22,26 +22,24 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers
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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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()
@@ -131,19 +248,19 @@ class AssignmentsE2ETest: StudentTest() {
fun testLetterGradeTextAssignmentE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 1)
+ val data = seedData(teachers = 1, courses = 1, students = 1)
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.")
- val 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)
@@ -163,13 +280,13 @@ class AssignmentsE2ETest: StudentTest() {
fun testPercentageFileAssignmentWithCommentE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 1)
+ val data = seedData(teachers = 1, courses = 1, students = 1)
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Seeding 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)
@@ -186,17 +303,22 @@ class AssignmentsE2ETest: StudentTest() {
assignmentListPage.clickAssignment(percentageFileAssignment)
Log.d(PREPARATION_TAG, "Seed a text file.")
- val uploadInfo = uploadTextFile(courseId = course.id, assignmentId = percentageFileAssignment.id, token = student.token, fileUploadType = FileUploadType.ASSIGNMENT_SUBMISSION)
+ val uploadInfo = uploadTextFile(
+ courseId = course.id,
+ assignmentId = percentageFileAssignment.id,
+ token = student.token,
+ fileUploadType = FileUploadType.ASSIGNMENT_SUBMISSION
+ )
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()
@@ -224,62 +346,47 @@ 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)
fun testMultipleAssignmentsE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 1)
+ val data = seedData(teachers = 1, courses = 1, students = 1)
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seeding 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")
}
@@ -288,28 +395,28 @@ class AssignmentsE2ETest: StudentTest() {
@TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.E2E)
fun testFilterAndSortAssignmentsE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 1)
+ val data = seedData(teachers = 1, courses = 1, students = 1)
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Seeding 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)
@@ -402,44 +509,32 @@ 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)
fun testMediaCommentsE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 1)
+ val data = seedData(teachers = 1, courses = 1, students = 1)
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seeding 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.")
@@ -464,38 +559,38 @@ class AssignmentsE2ETest: StudentTest() {
fun testAddFileCommentE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 1)
+ val data = seedData(teachers = 1, courses = 1, students = 1)
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seeding 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,"Seed a comment attachment upload.")
val commentUploadInfo = uploadTextFile(
- assignmentId = assignment.id,
courseId = course.id,
+ assignmentId = assignment.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,"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)
@@ -510,31 +605,31 @@ class AssignmentsE2ETest: StudentTest() {
fun testSubmissionAttemptSelection() {
Log.d(PREPARATION_TAG, "Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 1)
+ val data = seedData(teachers = 1, courses = 1, students = 1)
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.")
- val pointsTextAssignment = 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()
@@ -542,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()
@@ -572,25 +667,25 @@ class AssignmentsE2ETest: StudentTest() {
fun testCommentsBelongToSubmissionAttempts() {
Log.d(PREPARATION_TAG,"Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 1)
+ val data = seedData(teachers = 1, courses = 1, students = 1)
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.")
- val pointsTextAssignment = 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)
@@ -606,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()
@@ -630,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"
@@ -665,27 +760,27 @@ class AssignmentsE2ETest: StudentTest() {
@TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.E2E)
fun showOnlyLetterGradeOnDashboardAndAssignmentListPageE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 1)
+ val data = seedData(teachers = 1, courses = 1, students = 1)
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val course = data.coursesList[0]
Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.")
- val pointsTextAssignment = 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)
@@ -694,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()
@@ -821,23 +916,23 @@ class AssignmentsE2ETest: StudentTest() {
@TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.E2E)
fun showOnlyLetterGradeOnGradesPageE2E() {
Log.d(PREPARATION_TAG, "Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 1)
+ val data = seedData(teachers = 1, courses = 1, students = 1)
val student = data.studentsList[0]
val teacher = data.teachersList[0]
val course = data.coursesList[0]
- Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.")
- val pointsTextAssignment = 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)
@@ -845,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()
@@ -903,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)
@@ -920,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 2e0ae5bba7..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
@@ -19,16 +19,17 @@ 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.model.*
+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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.ViewUtils
import com.instructure.student.ui.utils.seedData
@@ -54,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)
@@ -106,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 c1f0c7e521..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
@@ -2,11 +2,10 @@ package com.instructure.student.ui.e2e
import android.util.Log
import com.instructure.canvas.espresso.E2E
-import com.instructure.canvas.espresso.KnownBug
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.student.ui.pages.CollaborationsPage
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
@@ -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 90a7e6e01a..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
@@ -2,12 +2,12 @@ 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.Priority
+import com.instructure.canvas.espresso.TestCategory
+import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvas.espresso.refresh
import com.instructure.dataseeding.api.ConferencesApi
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
@@ -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 e0fe3bcffa..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
@@ -19,12 +19,12 @@ 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.dataseeding.api.ConversationsApi
import com.instructure.dataseeding.api.GroupsApi
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
@@ -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)
@@ -101,11 +98,11 @@ class DashboardE2ETest : StudentTest() {
dashboardPage.assertDisplaysGroup(group2, course1)
Log.d(STEP_TAG,"Click on 'All Courses' button. Assert that the All Courses Page is loaded.")
- dashboardPage.clickEditDashboard()
- editDashboardPage.assertPageObjects()
+ dashboardPage.openAllCoursesPage()
+ allCoursesPage.assertPageObjects()
Log.d(STEP_TAG, "Favorite '${course1.name}' course and navigate back to Dashboard Page.")
- editDashboardPage.favoriteCourse(course1.name)
+ allCoursesPage.favoriteCourse(course1.name)
Espresso.pressBack()
Log.d(STEP_TAG,"Assert that only the favoured course, '${course1.name}' is displayed." +
@@ -124,16 +121,16 @@ class DashboardE2ETest : StudentTest() {
Log.d(STEP_TAG,"Click on 'All Courses' button. Assert that the All Courses Page is loaded.")
dashboardPage.assertPageObjects()
- dashboardPage.clickEditDashboard()
- editDashboardPage.assertPageObjects()
+ dashboardPage.openAllCoursesPage()
+ allCoursesPage.assertPageObjects()
Log.d(STEP_TAG, "Assert that the mass select button's text is 'Unselect All', since one of the courses is selected.")
- editDashboardPage.assertCourseMassSelectButtonIsDisplayed(true)
+ allCoursesPage.assertCourseMassSelectButtonIsDisplayed(true)
Log.d(STEP_TAG, "Toggle off favourite star icon of '${course1.name}' course." +
"Assert that the 'mass' select button's label is 'Select All'.")
- editDashboardPage.unfavoriteCourse(course1.name)
- editDashboardPage.assertCourseMassSelectButtonIsDisplayed(false)
+ allCoursesPage.unfavoriteCourse(course1.name)
+ allCoursesPage.assertCourseMassSelectButtonIsDisplayed(false)
Log.d(STEP_TAG, "Navigate back to Dashboard Page.")
Espresso.pressBack()
@@ -186,15 +183,15 @@ class DashboardE2ETest : StudentTest() {
dashboardPage.assertCourseGrade(course2.name, "N/A")
Log.d(STEP_TAG,"Click on 'All Courses' button.")
- dashboardPage.clickEditDashboard()
- editDashboardPage.assertPageObjects()
+ dashboardPage.openAllCoursesPage()
+ allCoursesPage.assertPageObjects()
Log.d(STEP_TAG, "Assert that the group 'mass' select button's label is 'Select All'.")
- editDashboardPage.swipeUp()
- editDashboardPage.assertGroupMassSelectButtonIsDisplayed(false)
+ allCoursesPage.swipeUp()
+ allCoursesPage.assertGroupMassSelectButtonIsDisplayed(false)
Log.d(STEP_TAG, "Favorite '${group.name}' course and navigate back to Dashboard Page.")
- editDashboardPage.favoriteGroup(group.name)
+ allCoursesPage.favoriteGroup(group.name)
Espresso.pressBack()
Log.d(STEP_TAG,"Assert that only the favoured group, '${group.name}' is displayed." +
@@ -203,18 +200,18 @@ class DashboardE2ETest : StudentTest() {
dashboardPage.assertGroupNotDisplayed(group2)
Log.d(STEP_TAG,"Click on 'All Courses' button.")
- dashboardPage.clickEditDashboard()
- editDashboardPage.assertPageObjects()
+ dashboardPage.openAllCoursesPage()
+ allCoursesPage.assertPageObjects()
Thread.sleep(2000) //It can be flaky without this 2 seconds
- editDashboardPage.swipeUp()
+ allCoursesPage.swipeUp()
Log.d(STEP_TAG, "Assert that the group 'mass' select button's label is 'Unselect All'.")
- editDashboardPage.assertGroupMassSelectButtonIsDisplayed(true)
+ allCoursesPage.assertGroupMassSelectButtonIsDisplayed(true)
Log.d(STEP_TAG, "Toggle off favourite star icon of '${group.name}' group." +
"Assert that the 'mass' select button's label is 'Select All'.")
- editDashboardPage.unfavoriteGroup(group.name)
- editDashboardPage.assertGroupMassSelectButtonIsDisplayed(false)
+ allCoursesPage.unfavoriteGroup(group.name)
+ allCoursesPage.assertGroupMassSelectButtonIsDisplayed(false)
Log.d(STEP_TAG, "Navigate back to Dashboard Page.")
Espresso.pressBack()
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 85506cb028..cd1c0ae152 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
@@ -20,15 +20,13 @@ import android.os.SystemClock.sleep
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.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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+import com.instructure.espresso.getDateInCanvasFormat
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
@@ -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.")
@@ -156,24 +154,8 @@ class DiscussionsE2ETest: StudentTest() {
discussionListPage.assertUnreadReplyCount(newTopicName, 0)
Log.d(STEP_TAG, "Assert that the due date is the current date (in the expected format).")
- val currentDate = getCurrentDateInCanvasFormat()
+ val currentDate = getDateInCanvasFormat()
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 794c7dc02b..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
@@ -20,26 +20,25 @@ import android.os.Environment
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.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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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 2858633f86..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
@@ -4,13 +4,14 @@ import android.util.Log
import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers.withText
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.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
@@ -18,10 +19,6 @@ 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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.R
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
@@ -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 b2d370238f..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
@@ -21,14 +21,14 @@ import android.util.Log
import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers
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.ConversationsApi
import com.instructure.dataseeding.api.GroupsApi
-import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+import com.instructure.espresso.retryWithIncreasingDelay
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
@@ -45,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]
@@ -52,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)
@@ -67,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()
@@ -87,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." +
@@ -118,27 +121,30 @@ class InboxE2ETest: StudentTest() {
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()
- 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.")
+ retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) {
+ inboxPage.assertConversationNotStarred(seededConversation.subject)
+ }
+
+ 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")
@@ -148,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)
@@ -161,14 +167,15 @@ class InboxE2ETest: StudentTest() {
Log.d(STEP_TAG, "Select the conversation. Unarchive it, and assert that it has not displayed in the 'ARCHIVED' scope.")
inboxPage.selectConversations(listOf(seededConversation.subject))
inboxPage.clickUnArchive()
- inboxPage.assertConversationNotDisplayed(seededConversation.subject)
+
+ retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) {
+ inboxPage.assertConversationNotDisplayed(seededConversation.subject)
+ }
Log.d(STEP_TAG, "Navigate to 'STARRED' scope and assert that the conversations is displayed there.")
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)
@@ -186,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)
@@ -201,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()
@@ -214,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.")
@@ -225,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.")
@@ -237,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}.")
@@ -250,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)
@@ -266,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)
@@ -280,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]
@@ -287,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)
@@ -302,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()
@@ -342,11 +354,12 @@ 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")
- inboxPage.assertConversationDisplayed(seededConversation.subject)
+
+ retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) {
+ inboxPage.assertConversationDisplayed(seededConversation.subject)
+ }
Log.d(STEP_TAG, "Swipe '${seededConversation.subject}' left and assert it is removed from the 'STARRED' scope because it has became unstarred.")
inboxPage.swipeConversationLeft(seededConversation)
@@ -398,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()
@@ -406,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'.")
@@ -416,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 03c3a51ddc..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
@@ -18,6 +18,10 @@ 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.Priority
+import com.instructure.canvas.espresso.TestCategory
+import com.instructure.canvas.espresso.TestMetaData
import com.instructure.dataseeding.api.CoursesApi
import com.instructure.dataseeding.api.EnrollmentsApi
import com.instructure.dataseeding.api.SeedApi
@@ -27,10 +31,6 @@ import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.EnrollmentTypes.STUDENT_ENROLLMENT
import com.instructure.dataseeding.model.EnrollmentTypes.TEACHER_ENROLLMENT
import com.instructure.dataseeding.util.CanvasNetworkAdapter
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.ViewUtils
import com.instructure.student.ui.utils.seedData
@@ -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,45 +260,33 @@ 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.
+
if(lastSchoolSaved) {
Log.d(STEP_TAG,"Click 'Find Another School' button.")
loginLandingPage.clickFindAnotherSchoolButton()
@@ -309,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.")
@@ -322,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)
}
@@ -331,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 48f911a9a0..ad88ae8ba2 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
@@ -19,23 +19,21 @@ 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.dataseeding.api.AssignmentsApi
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
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
@@ -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 '${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(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 '${quiz1.title}' page with '${module2.name}' module.")
+ ModulesApi.createModuleItem(course.id, teacher.token, module2.id, page1.title, ModuleItemTypes.PAGE.stringVal, pageUrl = page1.url)
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,61 @@ 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 name: '${assignment1.name}'.")
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
- )
- }
+ Log.d(STEP_TAG, "Assert that the module name, '${module1.name}' is displayed at the bottom.")
+ assignmentDetailsPage.moduleItemInteractions.assertModuleNameDisplayed(module1.name)
- 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
- )
- }
+ Log.d(STEP_TAG, "Assert that the previous arrow button is not displayed because the user is on the first module item'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 '${quiz1.title}' quiz module item's 'Go To Quiz' page is displayed.")
+ assignmentDetailsPage.moduleItemInteractions.clickOnNextArrow()
+ goToQuizPage.assertQuizTitle(quiz1.title)
+
+ Log.d(STEP_TAG, "Assert that the module name, '${module1.name}' is displayed at the bottom.")
+ goToQuizPage.moduleItemInteractions.assertModuleNameDisplayed(module1.name)
+
+ Log.d(STEP_TAG, "Assert that both the previous and the next buttons are displayed (since we are not at the first or the last module item details page).")
+ goToQuizPage.moduleItemInteractions.assertPreviousArrowDisplayed()
+ goToQuizPage.moduleItemInteractions.assertNextArrowDisplayed()
+
+ Log.d(STEP_TAG, "Click on the next arrow button and assert that the Assignment Details Page is displayed with the corresponding assignment name: '${assignment2.name}'.")
+ goToQuizPage.moduleItemInteractions.clickOnNextArrow()
+ assignmentDetailsPage.assertPageObjects()
+ assignmentDetailsPage.assertAssignmentTitle(assignment2.name)
- 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
- )
- )
+ Log.d(STEP_TAG, "Assert that the second module name, '${module2.name}' is displayed at the bottom since we can navigate even between modules with these arrows.")
+ assignmentDetailsPage.moduleItemInteractions.assertModuleNameDisplayed(module2.name)
+
+ Log.d(STEP_TAG, "Assert that both the previous and the next buttons are displayed (since we are not at the first or the last module item details page, even if it's a first item of a module, but of the second module).")
+ assignmentDetailsPage.moduleItemInteractions.assertPreviousArrowDisplayed()
+ assignmentDetailsPage.moduleItemInteractions.assertNextArrowDisplayed()
+
+ Log.d(STEP_TAG, "Click on the next arrow button and assert that the '${discussionTopic1.title}' discussion topic module item's details page is displayed.")
+ assignmentDetailsPage.moduleItemInteractions.clickOnNextArrow()
+ discussionDetailsPage.assertTitleText(discussionTopic1.title)
+
+ Log.d(STEP_TAG, "Assert that the second module name, '${module2.name}' is displayed at the bottom.")
+ discussionDetailsPage.moduleItemInteractions.assertModuleNameDisplayed(module2.name)
+
+ Log.d(STEP_TAG, "Assert that both the previous and the next buttons are displayed.")
+ discussionDetailsPage.moduleItemInteractions.assertPreviousArrowDisplayed()
+ discussionDetailsPage.moduleItemInteractions.assertNextArrowDisplayed()
+
+ Log.d(STEP_TAG, "Click on the next arrow button and assert that the '${page1.url}' page module item's details page is displayed.")
+ discussionDetailsPage.moduleItemInteractions.clickOnNextArrow()
+ pageDetailsPage.webAssertPageUrl(page1.url)
+
+ Log.d(STEP_TAG, "Assert that the second module name, '${module2.name}' is displayed at the bottom.")
+ pageDetailsPage.moduleItemInteractions.assertModuleNameDisplayed(module2.name)
+
+ Log.d(STEP_TAG, "Assert that the previous arrow button is displayed but the next arrow button is not displayed because the user is on the last module item's details page.")
+ pageDetailsPage.moduleItemInteractions.assertPreviousArrowDisplayed()
+ pageDetailsPage.moduleItemInteractions.assertNextArrowNotDisplayed()
+ }
}
\ 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 80020b123d..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
@@ -19,14 +19,14 @@ package com.instructure.student.ui.e2e
import android.util.Log
import androidx.test.espresso.NoMatchingViewException
import com.instructure.canvas.espresso.E2E
-import com.instructure.canvas.espresso.ReleaseExclude
+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.QuizAnswer
import com.instructure.dataseeding.model.QuizQuestion
@@ -34,10 +34,6 @@ 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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
@@ -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 2f48aaacd3..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
@@ -20,14 +20,11 @@ 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
+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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
@@ -43,7 +40,7 @@ class PagesE2ETest: StudentTest() {
@E2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.PAGES, TestCategory.E2E, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.PAGES, TestCategory.E2E)
fun testPagesE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
@@ -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 = "
Regular Page Text
")
+ val pagePublished = PagesApi.createCoursePage(course.id, teacher.token, editingRoles = "teachers,students", body = "
Regular Page Text
")
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 = "
Regular Page Text
")
+ val pageNotEditable = PagesApi.createCoursePage(course.id, teacher.token, body = "
Regular Page Text
")
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 = "
Front Page Text
")
+ val pagePublishedFront = PagesApi.createCoursePage(course.id, teacher.token, frontPage = true, editingRoles = "public", body = "
Front Page Text
")
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 b63a31b4ab..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
@@ -18,10 +18,10 @@ package com.instructure.student.ui.e2e
import android.util.Log
import com.instructure.canvas.espresso.E2E
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.ViewUtils
import com.instructure.student.ui.utils.seedData
@@ -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 13261cd33e..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
@@ -28,19 +28,16 @@ import androidx.test.espresso.web.webdriver.DriverAtoms.getText
import androidx.test.espresso.web.webdriver.DriverAtoms.webScrollIntoView
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
import com.instructure.canvas.espresso.Stub
+import com.instructure.canvas.espresso.TestCategory
+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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.R
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
@@ -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.
@@ -65,7 +62,7 @@ class QuizzesE2ETest: StudentTest() {
@E2E
@Stub
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.PAGES, TestCategory.E2E, true)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.PAGES, TestCategory.E2E)
fun testQuizzesE2E() {
Log.d(PREPARATION_TAG, "Seeding data.")
@@ -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 cb42b97277..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
@@ -22,13 +22,13 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
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.canvasapi2.utils.RemoteConfigParam
import com.instructure.canvasapi2.utils.RemoteConfigUtils
import com.instructure.espresso.ViewUtils
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.R
import com.instructure.student.ui.utils.IntentActionMatcher
import com.instructure.student.ui.utils.StudentTest
@@ -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 e4cdad1e9c..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
@@ -18,18 +18,16 @@ 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.Priority
+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
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
@@ -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 2c98efb118..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,22 +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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.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
@@ -45,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,
@@ -57,37 +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..")
- todoPage.assertAssignmentDisplayed(testAssignment)
- todoPage.assertAssignmentDisplayed(borderDateAssignment)
-
- 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..")
- 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(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(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()
@@ -113,13 +109,13 @@ 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.clickEditDashboard()
- editDashboardPage.favoriteCourse(favoriteCourse.name)
+ dashboardPage.openAllCoursesPage()
+ allCoursesPage.favoriteCourse(favoriteCourse.name)
Log.d(STEP_TAG, "Navigate back to the Dashboard Page and open the To Do Page again.")
Espresso.pressBack()
@@ -139,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 4b675a8b75..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
@@ -19,19 +19,20 @@ package com.instructure.student.ui.e2e.k5
import android.util.Log
import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
-import com.instructure.canvasapi2.utils.toApiString
+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.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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.R
import com.instructure.student.ui.pages.ElementaryDashboardPage
import com.instructure.student.ui.utils.StudentTest
@@ -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 6088a3f2f7..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
@@ -19,19 +19,18 @@ package com.instructure.student.ui.e2e.k5
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.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
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.iso8601
import com.instructure.espresso.page.getStringFromResource
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.R
import com.instructure.student.ui.pages.ElementaryDashboardPage
import com.instructure.student.ui.utils.StudentTest
@@ -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 54c3387d3e..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
@@ -19,19 +19,17 @@ package com.instructure.student.ui.e2e.k5
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.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
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.ElementaryDashboardPage
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedDataForK5
@@ -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 a27cf7bd4b..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
@@ -19,11 +19,12 @@ package com.instructure.student.ui.e2e.k5
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.SecondaryFeatureCategory
+import com.instructure.canvas.espresso.TestCategory
+import com.instructure.canvas.espresso.TestMetaData
import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.ElementaryDashboardPage
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedDataForK5
@@ -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 2548685af9..d2dd8fb9c2 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
@@ -19,18 +19,19 @@ package com.instructure.student.ui.e2e.k5
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.ReleaseExclude
+import com.instructure.canvas.espresso.SecondaryFeatureCategory
+import com.instructure.canvas.espresso.Stub
+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
import com.instructure.espresso.page.withAncestor
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.R
import com.instructure.student.ui.pages.ElementaryDashboardPage
import com.instructure.student.ui.utils.StudentTest
@@ -40,6 +41,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 +52,13 @@ 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.
+ @Stub
@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 +78,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 +92,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 +110,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 +161,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 +218,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/DashboardE2EOfflineTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt
deleted file mode 100644
index 599a31aa0a..0000000000
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.
- * 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 com.instructure.canvas.espresso.OfflineE2E
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
-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
-
-@HiltAndroidTest
-class DashboardE2EOfflineTest : StudentTest() {
- override fun displaysPageObjects() = Unit
-
- override fun enableAndConfigureAccessibilityChecks() = Unit
-
- @OfflineE2E
- @Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E)
- fun testOfflineDashboardE2E() {
- Log.d(PREPARATION_TAG,"Seeding data.")
- val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1)
- val student = data.studentsList[0]
- val course1 = data.coursesList[0]
- val course2 = data.coursesList[1]
- val testAnnouncement = data.announcementsList[0]
-
- 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.")
- 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(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.")
- OfflineTestUtils.turnOffConnectionViaADB()
-
- 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 cours card.")
- dashboardPage.assertCourseOfflineSyncIconVisible(course1.name)
- dashboardPage.assertCourseOfflineSyncIconGone(course2.name)
-
- Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.")
- dashboardPage.selectCourse(course1)
- courseBrowserPage.selectAnnouncements()
-
- Log.d(STEP_TAG,"Assert that the '${testAnnouncement.title}' titled announcement is displayed, so the user is able to see it in offline mode because it was synced.")
- announcementListPage.assertTopicDisplayed(testAnnouncement.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.")
- OfflineTestUtils.turnOnConnectionViaADB()
- }
-
-}
\ No newline at end of file
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 5ca6d91392..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
@@ -19,12 +19,13 @@ package com.instructure.student.ui.e2e.offline
import android.util.Log
import androidx.test.espresso.Espresso
import com.google.android.material.checkbox.MaterialCheckBox
+import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.OfflineE2E
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
-import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils
+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
@@ -34,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,9 +50,8 @@ class ManageOfflineContentE2ETest : StudentTest() {
val student = data.studentsList[0]
val course1 = data.coursesList[0]
val course2 = data.coursesList[1]
- val testAnnouncement = data.announcementsList[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()
@@ -75,6 +76,12 @@ class ManageOfflineContentE2ETest : StudentTest() {
Log.d(STEP_TAG, "Assert that the tool bar texts are displayed properly, so the subtitle is '${course1.name}', because we are on the Manage Offline Content page of '${course1.name}' course.")
manageOfflineContentPage.assertToolbarTexts(course1.name)
+ Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state became 'Checked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_CHECKED)
+
+ Log.d(STEP_TAG, "Expand '${course1.name}' course.")
+ manageOfflineContentPage.expandCollapseItem(course1.name)
+
Log.d(STEP_TAG, "Deselect the 'Announcements' and 'Discussions' of the '${course1.name}' course.")
manageOfflineContentPage.changeItemSelectionState("Announcements")
manageOfflineContentPage.changeItemSelectionState("Discussions")
@@ -131,7 +138,7 @@ class ManageOfflineContentE2ETest : StudentTest() {
manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_UNCHECKED)
manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_UNCHECKED)
- Log.d(STEP_TAG, "Assert that the 'SELECT ALL' will be displayed after clicking the 'SELECT ALL' button.")
+ Log.d(STEP_TAG, "Assert that the 'SELECT ALL' will be displayed after clicking the 'DESELECT ALL' button.")
manageOfflineContentPage.assertSelectButtonText(selectAll = true)
Log.d(STEP_TAG, "Navigate back to Dashboard Page. Open 'Global' Manage Offline Content page.")
@@ -185,20 +192,18 @@ class ManageOfflineContentE2ETest : StudentTest() {
Log.d(STEP_TAG, "Assert that both of the seeded courses are displayed as a selectable item in the Manage Offline Content page.")
manageOfflineContentPage.assertCourseCountWithMatcher(2)
- Log.d(STEP_TAG, "Click on the 'Sync' button.")
+ 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.")
- OfflineTestUtils.turnOffConnectionViaADB()
+ 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 '${course2.name}' course and open 'Grades' menu to check if it's really synced and can be seen in offline mode.")
@@ -212,7 +217,7 @@ class ManageOfflineContentE2ETest : StudentTest() {
@After
fun tearDown() {
Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.")
- OfflineTestUtils.turnOnConnectionViaADB()
+ turnOnConnectionViaADB()
}
}
\ No newline at end of file
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
new file mode 100644
index 0000000000..cb99b21f54
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ * 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.matcher.ViewMatchers
+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
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.After
+import org.junit.Test
+
+@HiltAndroidTest
+class OfflineAllCoursesE2ETest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() = Unit
+
+ @OfflineE2E
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E, SecondaryFeatureCategory.ALL_COURSES)
+ fun testOfflineAllCoursesE2E() {
+
+ Log.d(PREPARATION_TAG, "Seeding data.")
+ val data = seedData(students = 1, teachers = 1, courses = 3, announcements = 1)
+ val student = data.studentsList[0]
+ val course1 = data.coursesList[0]
+ val course2 = data.coursesList[1]
+ val course3 = data.coursesList[2]
+
+ Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.")
+ tokenLogin(student)
+ dashboardPage.waitForRender()
+
+ Log.d(STEP_TAG, "Open the 'All Courses' page and wait for it to be rendered.")
+ dashboardPage.openAllCoursesPage()
+ allCoursesPage.assertPageObjects()
+
+ Log.d(STEP_TAG, "Favourite '${course1.name}' course and assert if it became favourited. Then navigate back to Dashboard page.")
+ allCoursesPage.favoriteCourse(course1.name)
+ allCoursesPage.assertCourseFavorited(course1)
+ Espresso.pressBack()
+
+ Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.")
+ dashboardPage.openGlobalManageOfflineContentPage()
+ manageOfflineContentPage.assertPageObjects()
+
+ Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state is 'Unchecked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED)
+ manageOfflineContentPage.assertCheckedStateOfItem(course2.name, MaterialCheckBox.STATE_UNCHECKED)
+
+ Log.d(STEP_TAG, "Select '${course1.name}' and '${course2.name}' courses' checkboxes and Sync them.")
+ manageOfflineContentPage.changeItemSelectionState(course1.name)
+ manageOfflineContentPage.changeItemSelectionState(course2.name)
+ manageOfflineContentPage.clickOnSyncButtonAndConfirm()
+
+ 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()
+ 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.assertDisplaysCourse(course1)
+ dashboardPage.assertCourseNotDisplayed(course2)
+ dashboardPage.assertCourseNotDisplayed(course3)
+
+ Log.d(STEP_TAG, "Open the 'All Courses' page and wait for it to be rendered.")
+ dashboardPage.openAllCoursesPage()
+ allCoursesPage.assertPageObjects()
+
+ Log.d(STEP_TAG, "Assert that the plus 'Note' box is displayed in which warns the user that favouring courses can only be done in online mode.")
+ allCoursesPage.assertOfflineNoteDisplayed()
+
+ Log.d(STEP_TAG, "Dismiss the offline 'Note' box and assert if it's disappear.")
+ allCoursesPage.dismissOfflineNoteBox()
+ allCoursesPage.assertOfflineNoteNotDisplayed()
+
+ Log.d(STEP_TAG, "Assert that the select/unselect all button is not clickable because offline mode does not supports it.")
+ allCoursesPage.assertSelectUnselectAllButtonNotClickable()
+
+ Log.d(STEP_TAG, "Try to unfavorite '${course1.name}' course and assert it does not happened because favoring does not allowed in offline state.")
+ allCoursesPage.unfavoriteCourse(course1.name)
+ allCoursesPage.assertCourseFavorited(course1)
+
+ Log.d(STEP_TAG, "Assert that '${course3.name}' course's details are faded (and they having 0.4 alpha value) and it's offline sync icon is not displayed since it's not synced.")
+ allCoursesPage.assertCourseDetailsAlpha(course3.name, 0.4f)
+ allCoursesPage.assertCourseOfflineSyncButton(course3.name, ViewMatchers.Visibility.GONE)
+
+ Log.d(STEP_TAG, "Assert that '${course1.name}' course's favourite star is faded (and it's having 0.4 alpha value) because favoring is not possible in offline mode," +
+ "the course title and open button are not faded (1.0 alpha value) and the offline sync icon is displayed since the course is synced.")
+ allCoursesPage.assertCourseFavouriteStarAlpha(course1.name, 0.4f)
+ allCoursesPage.assertCourseTitleAlpha(course1.name, 1.0f)
+ allCoursesPage.assertCourseOpenButtonAlpha(course1.name, 1.0f)
+ allCoursesPage.assertCourseOfflineSyncButton(course1.name, ViewMatchers.Visibility.VISIBLE)
+
+ 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
+ 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/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
new file mode 100644
index 0000000000..017eed51df
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ * 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.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
+import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.After
+import org.junit.Test
+
+@HiltAndroidTest
+class OfflineCourseBrowserE2ETest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() = Unit
+
+ @OfflineE2E
+ @Test
+ @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 course = data.coursesList[0]
+
+ 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, "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, "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, "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")
+ var disabledTabs = arrayOf("Discussions", "Grades", "People", "Syllabus", "BigBlueButton")
+ assertTabsEnabled(courseBrowserPage, enabledTabs)
+ assertTabsDisabled(courseBrowserPage, disabledTabs)
+
+ 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.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 '${course.name}' course for sync.")
+ manageOfflineContentPage.changeItemSelectionState(course.name)
+ 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()
+
+ 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")
+ disabledTabs = arrayOf("Google Drive", "Collaborations")
+ assertTabsEnabled(courseBrowserPage, enabledTabs)
+ assertTabsDisabled(courseBrowserPage, disabledTabs)
+ }
+
+ @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()
+ }
+
+ private fun assertTabsEnabled(courseBrowserPage: CourseBrowserPage, tabs: Array) {
+ tabs.forEach { tab ->
+ courseBrowserPage.assertTabEnabled(tab)
+ }
+ }
+
+ private fun assertTabsDisabled(courseBrowserPage: CourseBrowserPage, tabs: Array) {
+ tabs.forEach { tab ->
+ courseBrowserPage.assertTabDisabled(tab)
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..c49034b82d
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt
@@ -0,0 +1,156 @@
+/*
+ * 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.
+ * 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.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
+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
+
+@HiltAndroidTest
+class OfflineDashboardE2ETest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() = Unit
+
+ @OfflineE2E
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
+ fun testOfflineDashboardE2E() {
+
+ Log.d(PREPARATION_TAG,"Seeding data.")
+ val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1)
+ val student = data.studentsList[0]
+ val course1 = data.coursesList[0]
+ val course2 = data.coursesList[1]
+ val testAnnouncement = data.announcementsList[0]
+
+ 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, "Assert that the '${course1.name}' course's checkbox state is 'Unchecked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED)
+
+ Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.")
+ manageOfflineContentPage.changeItemSelectionState(course1.name)
+ manageOfflineContentPage.clickOnSyncButtonAndConfirm()
+
+ 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()
+ 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.")
+ dashboardPage.assertCourseOfflineSyncIconVisible(course1.name)
+ dashboardPage.assertCourseOfflineSyncIconGone(course2.name)
+
+ Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.")
+ dashboardPage.selectCourse(course1)
+ courseBrowserPage.selectAnnouncements()
+
+ Log.d(STEP_TAG,"Assert that the '${testAnnouncement.title}' titled announcement is displayed, so the user is able to see it in offline mode because it was synced.")
+ announcementListPage.assertTopicDisplayed(testAnnouncement.title)
+ }
+
+ @OfflineE2E
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
+ fun testOfflineDashboardUnavailableFeaturesE2E() {
+
+ Log.d(PREPARATION_TAG,"Seeding data.")
+ val data = seedData(students = 1, teachers = 1, courses = 1)
+ val student = data.studentsList[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}.")
+ 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, "Select the entire '${course.name}' course for sync. Click on the 'Sync' button.")
+ manageOfflineContentPage.changeItemSelectionState(course.name)
+ 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, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.")
+ OfflineTestUtils.assertOfflineIndicator()
+
+ Log.d(STEP_TAG, "Assert that the bottom menus (except Dashboard) are disabled and unavailable in offline mode.")
+ dashboardPage.assertBottomMenusAreDisabled()
+
+ Log.d(STEP_TAG, "Try to open the '${course.name}' course's more menu of the Dashboard Page. Assert that the 'No Internet Connection' dialog is displayed. Dismiss it after the assertion.")
+ dashboardPage.clickOnCourseOverflowButton(course.name)
+ OfflineTestUtils.assertNoInternetConnectionDialog()
+ OfflineTestUtils.dismissNoInternetConnectionDialog()
+
+ Log.d(STEP_TAG, "Try to open the global 'Manage Offline Content' page via the more menu of the Dashboard Page. Assert that the 'No Internet Connection' dialog is displayed. Dismiss it after the assertion.")
+ Thread.sleep(5000) //Wait for the system notification to disappear, because it overlaps the More menu button on the toolbar.
+ dashboardPage.openGlobalManageOfflineContentPage()
+ 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/OfflineDiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt
new file mode 100644
index 0000000000..37fad33e9d
--- /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.getDateInCanvasFormat
+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 = getDateInCanvasFormat()
+ 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
new file mode 100644
index 0000000000..1670437cc1
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineFilesE2ETest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.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
+import com.instructure.student.ui.utils.uploadTextFile
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.After
+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, SecondaryFeatureCategory.OFFLINE_MODE)
+ fun testOfflineFilesE2E() {
+
+ Log.d(PREPARATION_TAG,"Seeding data.")
+ val data = seedData(teachers = 1, courses = 1, students = 1)
+ val student = data.studentsList[0]
+ val teacher = data.teachersList[0]
+ val course = data.coursesList[0]
+
+ 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, 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)
+
+ 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}'.")
+ 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 'Files' of '${course.name}' course for sync. Click on the 'Sync' button.")
+ manageOfflineContentPage.expandCollapseItem(course.name)
+ manageOfflineContentPage.changeItemSelectionState("Files")
+ 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, "Select '${course.name}' course and click on 'Files' tab to navigate to the File List Page.")
+ dashboardPage.selectCourse(course)
+ courseBrowserPage.selectFiles()
+
+ Log.d(STEP_TAG, "Assert that under the 'Files' tab there are 2 items, a folder and a file which has been seeded recently, and there is 1 item within the folder.")
+ fileListPage.assertFileListCount(2)
+ fileListPage.assertFolderSize(courseTestFolder.name, 1)
+
+ Log.d(STEP_TAG, "Assert that the folder's name is '${courseTestFolder.name}' and the file's name is '${rootFolderTestTextFile.fileName}'.")
+ fileListPage.assertItemDisplayed(rootFolderTestTextFile.fileName)
+ fileListPage.assertItemDisplayed(courseTestFolder.name)
+
+ Log.d(STEP_TAG, "Open '${courseTestFolder.name}' folder and assert that the '${courseTestFolderTextFile.fileName}' file is displayed within it. Navigate back to File List Page.")
+ fileListPage.selectItem(courseTestFolder.name)
+ fileListPage.assertItemDisplayed(courseTestFolderTextFile.fileName)
+ Espresso.pressBack()
+
+ Log.d(STEP_TAG, "Click on 'Search' (magnifying glass) icon and type '${rootFolderTestTextFile.fileName}', the file's name to the search input field.")
+ fileListPage.searchable.clickOnSearchButton()
+ fileListPage.searchable.typeToSearchBar(rootFolderTestTextFile.fileName)
+
+ Log.d(STEP_TAG, "Assert that only 1 file matches for the search text, and it is '${rootFolderTestTextFile.fileName}', and no directories has been shown in the result.")
+ fileListPage.assertSearchResultCount(1)
+ fileListPage.assertSearchItemDisplayed(rootFolderTestTextFile.fileName)
+ fileListPage.assertItemNotDisplayed(testCourseFolderName)
+ Espresso.closeSoftKeyboard()
+ }
+
+ @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/OfflineLeftSideMenuE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt
new file mode 100644
index 0000000000..84bcbdd3be
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ * 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 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
+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 OfflineLeftSideMenuE2ETest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() = Unit
+
+ @OfflineE2E
+ @Test
+ @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 = 1, announcements = 1)
+ val student = data.studentsList[0]
+
+ 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()
+ 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/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.")
+ leftSideNavigationDrawerPage.assertOfflineIndicatorDisplayed()
+
+ Log.d(STEP_TAG, "Assert that the 'Files, Bookmarks, Studio, Color Overlay, Help' menus are disabled in offline mode.")
+ leftSideNavigationDrawerPage.assertOfflineDisabledMenus(0.5f)
+
+ Log.d(STEP_TAG, "Assert that the 'Settings, Show Grades, Change User, Log Out' menus are enabled in offline mode because they are supported.")
+ leftSideNavigationDrawerPage.assertOfflineEnabledMenus(1.0f)
+
+ Log.d(STEP_TAG, "Click on 'Files' menu and assert that the 'No Internet Connection' dialog is popping-up. Dismiss it.")
+ leftSideNavigationDrawerPage.clickFilesMenu()
+ assertNoInternetConnectionDialog()
+ dismissNoInternetConnectionDialog()
+
+ Log.d(STEP_TAG, "Click on 'Bookmarks' menu and assert that the 'No Internet Connection' dialog is popping-up. Dismiss it.")
+ leftSideNavigationDrawerPage.clickBookmarksMenu()
+ assertNoInternetConnectionDialog()
+ dismissNoInternetConnectionDialog()
+
+ Log.d(STEP_TAG, "Click on 'Studio' menu and assert that the 'No Internet Connection' dialog is popping-up. Dismiss it.")
+ leftSideNavigationDrawerPage.clickStudioMenu()
+ assertNoInternetConnectionDialog()
+ dismissNoInternetConnectionDialog()
+
+ Log.d(STEP_TAG, "Click on 'Help' menu and assert that the 'No Internet Connection' dialog is popping-up. Dismiss it.")
+ leftSideNavigationDrawerPage.clickHelpMenu()
+ assertNoInternetConnectionDialog()
+ dismissNoInternetConnectionDialog()
+ }
+
+ @After
+ fun tearDown() {
+ Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, 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/OfflineLoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt
new file mode 100644
index 0000000000..810839655b
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ * 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 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.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
+import org.junit.After
+import org.junit.Test
+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.OFFLINE_MODE)
+ fun testOfflineChangeUserE2E() {
+
+ Log.d(PREPARATION_TAG, "Seeding data.")
+ val data = seedData(students = 2, teachers = 1, courses = 1)
+ val student1 = data.studentsList[0]
+ val student2 = data.studentsList[1]
+
+ Log.d(STEP_TAG, "Login with user: '${student1.name}', login id: '${student1.loginId}'.")
+ loginWithUser(student1)
+ dashboardPage.waitForRender()
+
+ Log.d(STEP_TAG, "Assert that the Offline indicator is not displayed because we are in online mode yet.")
+ dashboardPage.assertOfflineIndicatorNotDisplayed()
+
+ 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}'.")
+ 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.")
+ dashboardPage.assertOfflineIndicatorNotDisplayed()
+
+ 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, "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.")
+ loginLandingPage.assertDisplaysPreviousLogins()
+ loginLandingPage.assertPreviousLoginUserDisplayed(student1.name)
+ loginLandingPage.assertPreviousLoginUserDisplayed(student2.name)
+
+ Log.d(STEP_TAG, "Try to click on the last saved school's button and assert that the 'No Internet Connection' dialog will be displayed. Dismiss the dialog.")
+ loginLandingPage.clickOnLastSavedSchoolButton()
+ assertNoInternetConnectionDialog()
+ dismissNoInternetConnectionDialog()
+
+ Log.d(STEP_TAG, "Try to click on the 'Find another school' button and assert that the 'No Internet Connection' dialog will be displayed. Dismiss the dialog.")
+ loginLandingPage.clickFindAnotherSchoolButton()
+ 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.")
+ 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.")
+ dashboardPage.waitForRender()
+ dashboardPage.assertOfflineIndicatorDisplayed()
+
+ 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.")
+ 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) {
+
+ sleep(5100) //Need to wait > 5 seconds before each login attempt because of new 'too many attempts' login policy on web.
+
+ if(lastSchoolSaved) {
+ Log.d(STEP_TAG,"Click 'Find Another School' button.")
+ loginLandingPage.clickFindAnotherSchoolButton()
+ }
+ else {
+ Log.d(STEP_TAG, "Click 'Find My School' button.")
+ loginLandingPage.clickFindMySchoolButton()
+ }
+
+ Log.d(STEP_TAG,"Enter domain: '${user.domain}'.")
+ loginFindSchoolPage.enterDomain(user.domain)
+
+ Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.")
+ loginFindSchoolPage.clickToolbarNextMenuItem()
+ loginSignInPage.loginAs(user)
+ }
+
+ @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/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 = "
Regular Page Text
")
+
+ Log.d(PREPARATION_TAG,"Seed a PUBLISHED, but NOT editable page for '${course.name}' course.")
+ val pageNotEditable = PagesApi.createCoursePage(course.id, teacher.token, body = "
Regular Page Text
")
+
+ 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 = "
Front Page Text
")
+
+ 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
new file mode 100644
index 0000000000..a48ff20b91
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePeopleE2ETest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.matcher.ViewMatchers
+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.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 OfflinePeopleE2ETest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() = Unit
+
+ @OfflineE2E
+ @Test
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.PEOPLE, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
+ fun testOfflinePeopleE2E() {
+
+ 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]
+
+ 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 'People' of '${course.name}' course for sync. Click on the 'Sync' button.")
+ manageOfflineContentPage.changeItemSelectionState("People")
+ 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 'People' menu.")
+ dashboardPage.selectCourse(course)
+ courseBrowserPage.selectPeople()
+
+ Log.d(STEP_TAG, "Select '${teacher.name}' teacher.")
+ peopleListPage.selectPerson(teacher)
+
+ Log.d(STEP_TAG, "Assert that the compose message icon is not displayed (GONE) because it is not supported in offline mode.")
+ personDetailsPage.assertComposeMessageIcon(ViewMatchers.Visibility.GONE)
+ }
+
+ @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/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt
index 77cea19bc2..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,11 +18,13 @@ package com.instructure.student.ui.e2e.offline
import android.util.Log
import androidx.test.espresso.Espresso
+import com.google.android.material.checkbox.MaterialCheckBox
+import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.OfflineE2E
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.utils.StudentTest
import com.instructure.student.ui.utils.seedData
@@ -33,62 +35,86 @@ 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(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, "Assert that the '${course1.name}' course's checkbox state is 'Unchecked'.")
+ manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED)
+
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.")
- OfflineTestUtils.turnOffConnectionViaADB()
+ 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, "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)
@@ -101,7 +127,7 @@ class OfflineSyncProgressE2ETest : StudentTest() {
@After
fun tearDown() {
Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.")
- OfflineTestUtils.turnOnConnectionViaADB()
+ turnOnConnectionViaADB()
}
}
\ No newline at end of file
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
new file mode 100644
index 0000000000..6efa56db19
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ * 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 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
+import com.instructure.student.ui.utils.StudentTest
+import com.instructure.student.ui.utils.ViewUtils
+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 OfflineSyncSettingsE2ETest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() = Unit
+
+ @OfflineE2E
+ @Test
+ @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}'.")
+ tokenLogin(student)
+ dashboardPage.waitForRender()
+
+ Log.d(STEP_TAG, "Open Settings page from the Left Side menu.")
+ leftSideNavigationDrawerPage.clickSettingsMenu()
+
+ Log.d(STEP_TAG, "Assert that the Offline Sync Settings related information is displayed properly on the Settings Page ('Daily' is the default status).")
+ settingsPage.assertOfflineContentDisplayed()
+ settingsPage.assertOfflineContentTitle()
+ settingsPage.assertOfflineSyncSettingsStatus(R.string.daily)
+
+ Log.d(STEP_TAG, "Open Offline Sync Settings page and wait for it to be loaded.")
+ settingsPage.openOfflineSyncSettingsPage()
+ offlineSyncSettingsPage.assertPageObjects()
+
+ Log.d(STEP_TAG, "Assert that further settings, such as the toolbar title is displayed and correct, and both the Auto Content Sync and Wi-Fi Only Sync toggles are displayed and checked by default.")
+ offlineSyncSettingsPage.assertFurtherSettingsIsDisplayed()
+ offlineSyncSettingsPage.assertSyncSettingsToolbarTitle()
+ offlineSyncSettingsPage.assertAutoSyncSwitchIsChecked()
+ offlineSyncSettingsPage.assertWifiOnlySwitchIsChecked()
+
+ Log.d(STEP_TAG, "Assert that all the descriptions of how these settings are working are displayed.")
+ offlineSyncSettingsPage.assertSyncSettingsPageDescriptions()
+
+ Log.d(STEP_TAG, "Assert that the sync frequency label is 'Daily', because that is the default setting.")
+ offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.daily)
+
+ Log.d(STEP_TAG, "Switch off the 'Auto Content Sync' toggle, and assert if that the further settings below will disappear.")
+ offlineSyncSettingsPage.clickAutoSyncSwitch()
+
+ Log.d(STEP_TAG, "Assert that further settings, such as the toolbar title, Auto Content Sync and Wi-Fi Only Sync toggles are NOT displayed.")
+ offlineSyncSettingsPage.assertFurtherSettingsNotDisplayed()
+
+ Log.d(STEP_TAG, "Switch back the 'Auto Content Sync' toggle, and assert if that the further settings below will be displayed again.")
+ offlineSyncSettingsPage.clickAutoSyncSwitch()
+
+ Log.d(STEP_TAG, "Assert that further settings, such as the toolbar title is displayed and correct, and both the Auto Content Sync and Wi-Fi Only Sync toggles are displayed again.")
+ offlineSyncSettingsPage.assertFurtherSettingsIsDisplayed()
+
+ Log.d(STEP_TAG, "Switch off the 'Sync Content Wi-Fi Only' toggle and assert that the confirmation dialog (with the proper texts) is displayed.")
+ offlineSyncSettingsPage.clickWifiOnlySwitch()
+ offlineSyncSettingsPage.assertTurnOffWifiOnlyDialogTexts()
+
+ Log.d(STEP_TAG, "Click on the 'TURN OFF' button on the dialog to really turn off the 'Sync Content Wi-Fi Only' switch.")
+ offlineSyncSettingsPage.clickTurnOff()
+
+ Log.d(STEP_TAG, "Assert that the 'Sync Content Wi-Fi Only' switch is not checked any more.")
+ offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked()
+
+ Log.d(STEP_TAG, "Open the Sync Frequency Settings dialog and select 'Weekly' option.")
+ offlineSyncSettingsPage.openSyncFrequencySettingsDialog()
+ offlineSyncSettingsPage.clickSyncFrequencyDialogOption(R.string.weekly)
+
+ Log.d(STEP_TAG, "Assert that the sync frequency label became 'Weekly' (without any manual refresh).")
+ offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly)
+
+ Log.d(STEP_TAG, "Navigate back to Dashboard Page and logout.")
+ ViewUtils.pressBackButton(2)
+ leftSideNavigationDrawerPage.logout()
+
+ Log.d(STEP_TAG, "Click 'Find My School' button.")
+ loginLandingPage.clickFindMySchoolButton()
+
+ 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}'.")
+ loginSignInPage.loginAs(student)
+ dashboardPage.waitForRender()
+
+ Log.d(STEP_TAG, "Open Settings page from the Left Side menu.")
+ leftSideNavigationDrawerPage.clickSettingsMenu()
+
+ Log.d(STEP_TAG, "Assert that the Offline Sync Settings frequency text is 'Weekly' (because we set it previously).")
+ settingsPage.assertOfflineSyncSettingsStatus(R.string.weekly)
+
+ Log.d(STEP_TAG, "Open Offline Sync Settings page and wait for it to be loaded.")
+ settingsPage.openOfflineSyncSettingsPage()
+ offlineSyncSettingsPage.assertPageObjects()
+
+ Log.d(STEP_TAG, "Assert that the Offline Sync Settings frequency text is 'Weekly' (because we set it previously) and the 'Sync Content Wi-Fi Only' switch is switched off.")
+ offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly)
+ offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked()
+ }
+
+ @After
+ fun tearDown() {
+ Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, 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/utils/OfflineTestUtils.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt
index 717d37af98..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
@@ -16,14 +16,14 @@
*/
package com.instructure.student.ui.e2e.offline.utils
-import androidx.test.espresso.matcher.ViewMatchers.withChild
-import androidx.test.espresso.matcher.ViewMatchers.withId
-import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.click
import com.instructure.espresso.matchers.WaitForViewMatcher.waitForView
import com.instructure.espresso.page.plus
import com.instructure.student.R
@@ -31,18 +31,6 @@ import org.hamcrest.CoreMatchers.allOf
object OfflineTestUtils {
- fun turnOffConnectionViaADB() {
- val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
- device.executeShellCommand("svc wifi disable")
- device.executeShellCommand("svc data disable")
- }
-
- fun turnOnConnectionViaADB() {
- val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
- device.executeShellCommand("svc wifi enable")
- device.executeShellCommand("svc data enable")
- }
-
fun turnOffConnectionOnUI() {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@@ -80,4 +68,20 @@ object OfflineTestUtils {
)
).assertDisplayed()
}
+
+ fun assertNoInternetConnectionDialog() {
+ waitForView(withId(R.id.alertTitle) + withText(R.string.noInternetConnectionTitle)).assertDisplayed()
+ }
+
+ fun dismissNoInternetConnectionDialog() {
+ onView(withText(android.R.string.ok) + isDescendantOfA(withId(R.id.buttonPanel) +
+ 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 e9825d4c3d..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.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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.SecondaryFeatureCategory
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
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/AnnouncementInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt
index 187ff93b34..75dd998301 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt
@@ -17,6 +17,10 @@ package com.instructure.student.ui.interaction
import androidx.test.espresso.Espresso
import androidx.test.espresso.web.webdriver.Locator
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse
@@ -28,10 +32,6 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader
import com.instructure.canvasapi2.models.Group
import com.instructure.canvasapi2.models.Tab
import com.instructure.canvasapi2.models.User
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
@@ -52,7 +52,7 @@ class AnnouncementInteractionTest : StudentTest() {
// Student enrolled in intended section can see and reply to the announcement
// (This kind of seems like more of a test of the mocked endpoint, but we'll go with it.)
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION)
fun testAnnouncement_replyToSectionSpecificAnnouncement() {
val data = getToCourse(createSections = true)
@@ -90,7 +90,7 @@ class AnnouncementInteractionTest : StudentTest() {
// User can preview an announcement attachment
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION)
fun testAnnouncement_previewAttachment() {
val data = getToCourse()
@@ -132,7 +132,7 @@ class AnnouncementInteractionTest : StudentTest() {
// View/reply to an announcement
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION)
fun testAnnouncement_reply() {
val data = getToCourse()
@@ -156,7 +156,7 @@ class AnnouncementInteractionTest : StudentTest() {
// Tests that we can create an announcement (as teacher).
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION)
fun testAnnouncementCreate_base() {
val data = getToAnnouncementList()
@@ -170,7 +170,7 @@ class AnnouncementInteractionTest : StudentTest() {
// Tests code around closing / aborting announcement creation (as a teacher)
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION)
fun testAnnouncementCreate_abort() {
val data = getToAnnouncementList()
val course = data.courses.values.first()
@@ -188,7 +188,7 @@ class AnnouncementInteractionTest : StudentTest() {
// Tests code around creating an announcement with no description (as a teacher)
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.COMMON, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION)
fun testAnnouncementCreate_missingDescription() {
getToAnnouncementList()
@@ -198,7 +198,7 @@ class AnnouncementInteractionTest : StudentTest() {
// Tests code around creating an announcement with no title (as a teacher)
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.COMMON, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION)
fun testAnnouncementCreate_missingTitle() {
getToAnnouncementList()
discussionListPage.createAnnouncement("", "description")
@@ -206,7 +206,7 @@ class AnnouncementInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION)
fun testGroupAnnouncementCreateAsStudent() {
getToGroup()
@@ -217,7 +217,7 @@ class AnnouncementInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION)
fun testSearchAnnouncement() {
val data = getToAnnouncementList()
val course = data.courses.values.first()
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 b78a0e9db0..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
@@ -15,6 +15,12 @@
*/
package com.instructure.student.ui.interaction
+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.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addAssignment
import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups
@@ -23,25 +29,22 @@ import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CourseSettings
import com.instructure.canvasapi2.utils.toApiString
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.SecondaryFeatureCategory
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertNotNull
import org.junit.Test
-import java.util.Calendar
+import java.util.*
@HiltAndroidTest
class AssignmentDetailsInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.SUBMISSIONS_ONLINE_URL)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_ONLINE_URL)
fun testSubmission_submitAssignment() {
// TODO - Test submitting for each submission type
// For now, I'm going to just test one submission type
@@ -53,7 +56,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
val course = data.courses.values.first()
val student = data.students[0]
val token = data.tokenFor(student)!!
- val assignment = data.addAssignment(courseId = course.id, submissionType = Assignment.SubmissionType.ONLINE_URL)
+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_URL))
data.addSubmissionForAssignment(
assignmentId = assignment.id,
userId = data.users.values.first().id,
@@ -118,7 +121,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
@Test
@TestMetaData(Priority.COMMON, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
- fun testDisplayBookmarMenu() {
+ fun testDisplayBookmarkMenu() {
val data = setUpData()
goToAssignmentList()
val assignmentList = data.assignments
@@ -194,7 +197,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testLetterGradeAssignmentWithoutQuantitativeRestriction() {
val data = setUpData()
val assignment = addAssignment(data, Assignment.GradingType.LETTER_GRADE, "B", 90.0, 100)
@@ -207,7 +210,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testGpaScaleAssignmentWithoutQuantitativeRestriction() {
setUpData()
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.GPA_SCALE, "3.7", 90.0, 100)
@@ -220,7 +223,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPointsAssignmentWithoutQuantitativeRestriction() {
setUpData()
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, "90", 90.0, 100)
@@ -233,7 +236,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPointsAssignmentExcusedWithoutQuantitativeRestriction() {
setUpData()
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, null, 90.0, 100, excused = true)
@@ -246,7 +249,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPercentageAssignmentWithoutQuantitativeRestriction() {
setUpData()
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PERCENT, "90%", 90.0, 100)
@@ -259,7 +262,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPassFailAssignmentWithoutQuantitativeRestriction() {
setUpData()
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PASS_FAIL, "complete", 0.0, 0)
@@ -272,7 +275,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testLetterGradeAssignmentWithQuantitativeRestriction() {
val data = setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(data, Assignment.GradingType.LETTER_GRADE, "B", 90.0, 100)
@@ -285,7 +288,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testGpaScaleAssignmentWithQuantitativeRestriction() {
setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.GPA_SCALE, "3.7", 90.0, 100)
@@ -298,7 +301,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPointsAssignmentWithQuantitativeRestriction() {
setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, "65", 65.0, 100)
@@ -311,7 +314,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPointsAssignmentExcusedWithQuantitativeRestriction() {
setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, null, 90.0, 100, excused = true)
@@ -324,7 +327,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPercentageAssignmentWithQuantitativeRestriction() {
setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PERCENT, "70%", 70.0, 100)
@@ -337,7 +340,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPassFailAssignmentWithQuantitativeRestriction() {
setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PASS_FAIL, "complete", 0.0, 0)
@@ -349,6 +352,190 @@ class AssignmentDetailsInteractionTest : StudentTest() {
assignmentDetailsPage.assertScoreNotDisplayed()
}
+ @Test
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_MULTIPLE_TYPE)
+ fun testSubmission_multipleSubmissionType() {
+ val data = MockCanvas.init(
+ studentCount = 1,
+ courseCount = 1
+ )
+
+ val course = data.courses.values.first()
+ val student = data.students[0]
+ val token = data.tokenFor(student)!!
+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY, Assignment.SubmissionType.ONLINE_UPLOAD, Assignment.SubmissionType.MEDIA_RECORDING, Assignment.SubmissionType.DISCUSSION_TOPIC, Assignment.SubmissionType.ONLINE_URL))
+ data.addSubmissionForAssignment(
+ assignmentId = assignment.id,
+ userId = data.users.values.first().id,
+ type = Assignment.SubmissionType.ONLINE_URL.apiString
+ )
+ tokenLogin(data.domain, token, student)
+ routeTo("courses/${course.id}/assignments", data.domain)
+ assignmentListPage.waitForPage()
+
+ assignmentListPage.clickAssignment(assignment)
+ assignmentDetailsPage.clickSubmit()
+
+ assignmentDetailsPage.assertSubmissionTypeDisplayed("Text Entry")
+ assignmentDetailsPage.assertSubmissionTypeDisplayed("Website URL")
+ assignmentDetailsPage.assertSubmissionTypeDisplayed("File Upload")
+ assignmentDetailsPage.assertSubmissionTypeDisplayed("Media Recording")
+
+ //Try 1 submission to check if it's possible to submit even when there are multiple submission types available.
+ assignmentDetailsPage.selectSubmissionType(SubmissionType.ONLINE_URL)
+ urlSubmissionUploadPage.submitText("https://google.com")
+ 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(
@@ -401,7 +588,7 @@ class AssignmentDetailsInteractionTest : StudentTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
gradingType = Assignment.gradingTypeToAPIString(gradingType) ?: "",
pointsPossible = maxScore,
)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt
index b7c95d29ae..fab2876a38 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt
@@ -15,17 +15,18 @@
*/
package com.instructure.student.ui.interaction
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addAssignment
import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CourseSettings
-import com.instructure.student.ui.utils.*
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+import com.instructure.student.ui.utils.StudentTest
+import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
@@ -79,7 +80,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testLetterGradeAssignmentWithoutQuantitativeRestriction() {
setUpData()
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.LETTER_GRADE, "B", 90.0, 100)
@@ -89,7 +90,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testGpaScaleAssignmentWithoutQuantitativeRestriction() {
setUpData()
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.GPA_SCALE, "3.7", 90.0, 100)
@@ -99,7 +100,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPointsAssignmentWithoutQuantitativeRestriction() {
setUpData()
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, "90", 90.0, 100)
@@ -109,7 +110,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPointsAssignmentExcusedWithoutQuantitativeRestriction() {
setUpData()
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, null, 90.0, 100, excused = true)
@@ -119,7 +120,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPercentageAssignmentWithoutQuantitativeRestriction() {
setUpData()
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PERCENT, "90%", 90.0, 100)
@@ -129,7 +130,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPassFailAssignmentWithoutQuantitativeRestriction() {
setUpData()
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PASS_FAIL, "complete", 0.0, 0)
@@ -139,7 +140,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testLetterGradeAssignmentWithQuantitativeRestriction() {
setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.LETTER_GRADE, "B", 90.0, 100)
@@ -149,7 +150,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testGpaScaleAssignmentWithQuantitativeRestriction() {
setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.GPA_SCALE, "3.7", 90.0, 100)
@@ -159,7 +160,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPointsAssignmentWithQuantitativeRestriction() {
setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, "90", 90.0, 100)
@@ -169,7 +170,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPointsAssignmentExcusedWithQuantitativeRestriction() {
setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, null, 90.0, 100, excused = true)
@@ -179,7 +180,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPercentageAssignmentWithQuantitativeRestriction() {
setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PERCENT, "80%", 80.0, 100)
@@ -189,7 +190,7 @@ class AssignmentListInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
fun testPassFailAssignmentWithQuantitativeRestriction() {
setUpData(restrictQuantitativeData = true)
val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PASS_FAIL, "complete", 0.0, 0)
@@ -225,7 +226,7 @@ class AssignmentListInteractionTest : StudentTest() {
repeat(assignmentCount) {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
assignmentList.add(assignment)
}
@@ -253,7 +254,7 @@ class AssignmentListInteractionTest : StudentTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
gradingType = Assignment.gradingTypeToAPIString(gradingType) ?: "",
pointsPossible = maxScore,
)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt
index 2dc4dc3f22..ec8cac9747 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt
@@ -16,16 +16,16 @@
package com.instructure.student.ui.interaction
import androidx.test.espresso.Espresso
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
+import com.instructure.canvas.espresso.TestCategory
+import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addAssignment
import com.instructure.canvas.espresso.mockCanvas.addBookmark
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvas.espresso.refresh
import com.instructure.canvasapi2.models.Assignment
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -38,7 +38,7 @@ class BookmarkInteractionTest : StudentTest() {
// Test that we can create a bookmark via the UI and see it in the bookmark list
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.BOOKMARKS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.BOOKMARKS, TestCategory.INTERACTION)
fun testBookmark_create() {
val data = init()
val course = data.courses.values.first()
@@ -60,7 +60,7 @@ class BookmarkInteractionTest : StudentTest() {
// Tests that we can click a bookmark and end up in the bookmarked location
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.BOOKMARKS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.BOOKMARKS, TestCategory.INTERACTION)
fun testBookmark_click() {
val data = init()
val student = data.students.first()
@@ -83,7 +83,7 @@ class BookmarkInteractionTest : StudentTest() {
// Tests that we can change the name of a bookmark and still click through to the intended location
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.BOOKMARKS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.BOOKMARKS, TestCategory.INTERACTION)
fun testBookmark_changeName() {
val data = init()
val student = data.students.first()
@@ -115,7 +115,7 @@ class BookmarkInteractionTest : StudentTest() {
val course = data.courses.values.first()
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
val token = data.tokenFor(student)!!
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt
index ec8a032701..f989450db0 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt
@@ -17,6 +17,10 @@
package com.instructure.student.ui.interaction
import androidx.test.espresso.matcher.ViewMatchers
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addAssignment
import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment
@@ -25,12 +29,6 @@ import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CourseSettings
import com.instructure.canvasapi2.models.Grades
import com.instructure.canvasapi2.models.Tab
-import com.instructure.espresso.page.getStringFromResource
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
-import com.instructure.student.R
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -42,7 +40,7 @@ class CourseGradesInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testTotalGradeIsDisplayedWithGradeAndScoreWhenNotRestricted() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade("A", 100.0, data, false)
@@ -51,7 +49,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testTotalGradeIsDisplayedWithOnlyScoreWhenNotRestrictedAndThereIsNoGrade() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = false)
@@ -60,7 +58,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testGradeIsDisplayedWithOnlyGradeWhenQuantitativeDataIsRestricted() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade("A", 100.0, data, true)
@@ -69,7 +67,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testConvertedGradeIsDisplayedWithOnlyScoreWhenRestrictedAndThereIsNoGrade() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = true)
@@ -78,7 +76,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testLetterGradeAssignmentWithoutQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = false)
@@ -90,7 +88,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testGpaScaleAssignmentWithoutQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = false)
@@ -102,7 +100,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testPointsAssignmentWithoutQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = false)
@@ -114,7 +112,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testPointsAssignmentExcusedWithoutQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = false)
@@ -126,7 +124,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testPercentageAssignmentWithoutQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = false)
@@ -138,7 +136,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testPassFailAssignmentWithoutQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = false)
@@ -150,7 +148,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testLetterGradeAssignmentWithQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = true)
@@ -162,7 +160,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testGpaScaleAssignmentWithQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = true)
@@ -174,7 +172,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testPointsAssignmentWithQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = true)
@@ -186,7 +184,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testPointsAssignmentExcusedWithQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = true)
@@ -198,7 +196,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testPercentageAssignmentWithQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = true)
@@ -210,7 +208,7 @@ class CourseGradesInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION)
fun testPassFailAssignmentWithQuantitativeRestriction() {
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = true)
@@ -250,7 +248,7 @@ class CourseGradesInteractionTest : StudentTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
gradingType = Assignment.gradingTypeToAPIString(gradingType) ?: "",
pointsPossible = maxScore,
)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt
index 6f33a765b5..281aa55f3d 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt
@@ -22,16 +22,16 @@ import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
import androidx.test.espresso.web.webdriver.DriverAtoms.getText
import androidx.test.espresso.web.webdriver.DriverAtoms.webClick
import androidx.test.espresso.web.webdriver.Locator
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
+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.addFileToCourse
import com.instructure.canvas.espresso.mockCanvas.addPageToCourse
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.Tab
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -46,7 +46,7 @@ class CourseInteractionTest : StudentTest() {
// Link from a course page to another public course should open in the app
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.COURSE, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.COURSE, TestCategory.INTERACTION)
fun testCourse_linkFromCoursePageToPublicCoursePage() {
val data = getToCourse(courseCount = 2, favoriteCourseCount = 2)
@@ -81,7 +81,7 @@ class CourseInteractionTest : StudentTest() {
// user should be able to open/preview course file
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.FILES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.FILES, TestCategory.INTERACTION)
fun testCourse_openFile() {
// MBL-13499: Don't run this test on API 28 and above until we add HTTPS support
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt
index f02805dfff..8392395b13 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt
@@ -18,16 +18,16 @@ package com.instructure.student.ui.interaction
import androidx.lifecycle.MutableLiveData
import androidx.test.espresso.Espresso
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
+import com.instructure.canvas.espresso.TestCategory
+import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addAccountNotification
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.apis.EnrollmentAPI
import com.instructure.canvasapi2.models.CourseSettings
import com.instructure.canvasapi2.models.Grades
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.pandautils.di.NetworkStateProviderModule
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.espresso.fakes.FakeNetworkStateProvider
@@ -93,10 +93,10 @@ class DashboardInteractionTest : StudentTest() {
dashboardPage.assertCourseNotShown(nonFavorite)
dashboardPage.editFavorites()
- editDashboardPage.assertCourseDisplayed(nonFavorite)
- editDashboardPage.assertCourseNotFavorited(nonFavorite)
- editDashboardPage.favoriteCourse(nonFavorite)
- editDashboardPage.assertCourseFavorited(nonFavorite)
+ allCoursesPage.assertCourseDisplayed(nonFavorite)
+ allCoursesPage.assertCourseNotFavorited(nonFavorite)
+ allCoursesPage.favoriteCourse(nonFavorite)
+ allCoursesPage.assertCourseFavorited(nonFavorite)
Espresso.pressBack()
@@ -115,10 +115,10 @@ class DashboardInteractionTest : StudentTest() {
dashboardPage.assertDisplaysCourse(favorite)
dashboardPage.editFavorites()
- editDashboardPage.assertCourseDisplayed(favorite)
- editDashboardPage.assertCourseFavorited(favorite)
- editDashboardPage.unfavoriteCourse(favorite)
- editDashboardPage.assertCourseNotFavorited(favorite)
+ allCoursesPage.assertCourseDisplayed(favorite)
+ allCoursesPage.assertCourseFavorited(favorite)
+ allCoursesPage.unfavoriteCourse(favorite)
+ allCoursesPage.assertCourseNotFavorited(favorite)
Espresso.pressBack()
@@ -137,9 +137,9 @@ class DashboardInteractionTest : StudentTest() {
data.courses.values.forEach { dashboardPage.assertDisplaysCourse(it) }
dashboardPage.editFavorites()
- toFavorite.forEach { editDashboardPage.assertCourseNotFavorited(it) }
- editDashboardPage.selectAllCourses()
- toFavorite.forEach { editDashboardPage.assertCourseFavorited(it) }
+ toFavorite.forEach { allCoursesPage.assertCourseNotFavorited(it) }
+ allCoursesPage.selectAllCourses()
+ toFavorite.forEach { allCoursesPage.assertCourseFavorited(it) }
Espresso.pressBack()
@@ -156,9 +156,9 @@ class DashboardInteractionTest : StudentTest() {
toRemove.forEach { dashboardPage.assertDisplaysCourse(it) }
dashboardPage.editFavorites()
- toRemove.forEach { editDashboardPage.assertCourseFavorited(it) }
- editDashboardPage.unselectAllCourses()
- toRemove.forEach { editDashboardPage.assertCourseNotFavorited(it) }
+ toRemove.forEach { allCoursesPage.assertCourseFavorited(it) }
+ allCoursesPage.unselectAllCourses()
+ toRemove.forEach { allCoursesPage.assertCourseNotFavorited(it) }
Espresso.pressBack()
@@ -242,7 +242,7 @@ class DashboardInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.INTERACTION)
fun testDashboardCourses_tappingCourseCardDisplaysCourseBrowser() {
// Tapping on a course card opens course browser page
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
@@ -260,7 +260,7 @@ class DashboardInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.DASHBOARD, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.DASHBOARD, TestCategory.INTERACTION)
fun testDashboardCourses_gradeIsDisplayedWhenShowGradesIsSelected() {
// [Student] Grade is displayed when 'Show Grades' (located in navigation drawer) is selected
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
@@ -270,7 +270,7 @@ class DashboardInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.DASHBOARD, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.DASHBOARD, TestCategory.INTERACTION)
fun testDashboardCourses_gradeIsNotDisplayedWhenShowGradesIsDeSelected() {
// [Student] Grade is NOT displayed when 'Show Grades' (located in navigation drawer) is de-selected
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
@@ -280,7 +280,7 @@ class DashboardInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.DASHBOARD, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.DASHBOARD, TestCategory.INTERACTION)
fun testDashboardCourses_gradeIsDisplayedWithGradeAndScoreWhenNotRestricted() {
// [Student] Grade is displayed when 'Show Grades' (located in navigation drawer) is selected
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
@@ -291,7 +291,7 @@ class DashboardInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.DASHBOARD, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.DASHBOARD, TestCategory.INTERACTION)
fun testDashboardCourses_gradeIsDisplayedWithGradeOnlyWhenQuantitativeDataIsRestricted() {
// [Student] Grade is displayed when 'Show Grades' (located in navigation drawer) is selected
val data = setUpData(courseCount = 1, favoriteCourseCount = 1)
@@ -302,7 +302,7 @@ class DashboardInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testOfflineIndicatorDisplayedIfOffline() {
goToDashboard(setUpData())
@@ -312,7 +312,7 @@ class DashboardInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testOfflineIndicatorNotDisplayedIfOnline() {
goToDashboard(setUpData())
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt
index 8ae21f49ac..25310d5be8 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt
@@ -19,12 +19,23 @@ package com.instructure.student.ui.interaction
import android.os.SystemClock.sleep
import androidx.test.espresso.Espresso
import androidx.test.espresso.web.webdriver.Locator
-import com.instructure.canvas.espresso.mockCanvas.*
-import com.instructure.canvasapi2.models.*
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addAssignment
+import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
+import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse
+import com.instructure.canvas.espresso.mockCanvas.addFileToCourse
+import com.instructure.canvas.espresso.mockCanvas.addReplyToDiscussion
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CanvasContextPermission
+import com.instructure.canvasapi2.models.CourseSettings
+import com.instructure.canvasapi2.models.DiscussionEntry
+import com.instructure.canvasapi2.models.RemoteFile
+import com.instructure.canvasapi2.models.Tab
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
@@ -39,7 +50,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Verify that a discussion header shows up properly after discussion creation
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionCreate_base() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
@@ -58,7 +69,7 @@ class DiscussionsInteractionTest : StudentTest() {
// It's actually impossible to attach anything to a discussion topic with our app,
// so the attachment is done behind the scenes, after the fact.
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionCreate_withAttachment() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
val course = data.courses.values.first()
@@ -102,7 +113,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Test that you can't create a discussion when discussion creation is disabled
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionCreate_disabledWhenNotPermitted() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = false)
@@ -113,7 +124,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Tests that links to other Canvas content routes properly
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussion_linksRouteInApp() {
val data = getToCourse(studentCount = 2, courseCount = 2, enableDiscussionTopicCreation = true)
val course1 = data.courses.values.first()
@@ -141,7 +152,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Replies automatically get marked as read as the user scrolls through the list
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussion_postsGetMarkedAsRead() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
val course = data.courses.values.first()
@@ -180,7 +191,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Attachment is html, so that we can keep the viewing of it "in-house"
// NOTE: Very similar to testDiscussionCreate_withAttachment
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussion_previewAttachment() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
@@ -222,7 +233,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Tests that users can like entries and the correct like count is displayed, if the liking is enabled
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionLikePost_base() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
@@ -263,7 +274,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Tests that like count is shown if only graders can like
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionLikes_whenOnlyGradersCanRate() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
val course = data.courses.values.first()
@@ -300,7 +311,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Tests that discussion entry liking is not available when disabled
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionLikePost_disabledWhenNotPermitted() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
data.discussionRatingsEnabled = false
@@ -329,7 +340,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Test basic discussion view
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionView_base() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
val course1 = data.courses.values.first()
@@ -349,7 +360,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Test that you can reply to a discussion (if enabled)
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionView_replies() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
val course1 = data.courses.values.first()
@@ -376,7 +387,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Test that replies are not possible when they are not enabled
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionView_repliesHiddenWhenNotPermitted() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
val course1 = data.courses.values.first()
@@ -397,7 +408,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Test that a reply is displayed properly
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionReply_base() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
val course1 = data.courses.values.first()
@@ -425,7 +436,7 @@ class DiscussionsInteractionTest : StudentTest() {
// It is a whole other gear to manually specify an attachment the same way that a user would,
// so we add the attachments programmatically.
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionReply_withAttachment() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
val course1 = data.courses.values.first()
@@ -480,7 +491,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Tests that we can make a threaded reply to a reply
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionReply_threaded() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
val course1 = data.courses.values.first()
@@ -517,7 +528,7 @@ class DiscussionsInteractionTest : StudentTest() {
// It is a whole other gear to manually specify an attachment the same way that a user would,
// so we add the attachments programmatically.
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussionReply_threadedWithAttachment() {
val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true)
val course1 = data.courses.values.first()
@@ -581,7 +592,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Tests a discussion with a linked assignment.
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussion_linkedAssignment() {
val data = MockCanvas.init(teacherCount = 1, studentCount = 1, courseCount = 1, favoriteCourseCount = 1)
@@ -597,7 +608,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Add an assignment
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
name = assignmentName,
pointsPossible = 12
)
@@ -622,7 +633,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Tests a discussion with a linked assignment, show possible points if not restricted
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussion_showPointsIfNotRestricted() {
val data = MockCanvas.init(teacherCount = 1, studentCount = 1, courseCount = 1, favoriteCourseCount = 1)
@@ -638,7 +649,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Add an assignment
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
name = assignmentName,
pointsPossible = 12
)
@@ -667,7 +678,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Tests a discussion with a linked assignment, hide possible points if restricted
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION)
fun testDiscussion_hidePointsIfRestricted() {
val data = MockCanvas.init(teacherCount = 1, studentCount = 1, courseCount = 1, favoriteCourseCount = 1)
@@ -683,7 +694,7 @@ class DiscussionsInteractionTest : StudentTest() {
// Add an assignment
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
name = assignmentName,
pointsPossible = 12
)
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 8ea359fcf4..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
@@ -16,12 +16,12 @@
*/
package com.instructure.student.ui.interaction
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.init
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.ElementaryDashboardPage
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLoginElementary
@@ -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 122ec49631..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
@@ -17,16 +17,15 @@
package com.instructure.student.ui.interaction
import androidx.test.espresso.Espresso
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
+import com.instructure.canvas.espresso.TestCategory
+import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment
import com.instructure.canvas.espresso.mockCanvas.init
-import com.instructure.canvasapi2.models.CourseSettings
import com.instructure.canvasapi2.models.Enrollment
import com.instructure.espresso.page.getStringFromResource
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.R
import com.instructure.student.ui.pages.ElementaryDashboardPage
import com.instructure.student.ui.utils.StudentTest
@@ -40,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)
@@ -53,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)
@@ -73,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)
@@ -91,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)
@@ -105,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)
@@ -115,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)
@@ -136,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)
@@ -158,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/GroupLinksInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt
index aa1c84ccdf..007b295f5f 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt
@@ -18,6 +18,11 @@ package com.instructure.student.ui.interaction
import android.os.Build
import androidx.test.espresso.web.webdriver.Locator
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse
import com.instructure.canvas.espresso.mockCanvas.addFileToFolder
@@ -31,11 +36,6 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader
import com.instructure.canvasapi2.models.Group
import com.instructure.canvasapi2.models.Page
import com.instructure.canvasapi2.models.Tab
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.SecondaryFeatureCategory
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
@@ -58,7 +58,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to group opens group browser - eg: "/groups/:id"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION)
fun testGroupLink_base() {
setUpGroupAndSignIn()
dashboardPage.selectGroup(group)
@@ -67,7 +67,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to groups opens dashboard - eg: "/groups"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_DASHBOARD)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_DASHBOARD)
fun testGroupLink_dashboard() {
setUpGroupAndSignIn()
dashboardPage.assertDisplaysGroup(group, course)
@@ -75,7 +75,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Test not favorite group on dashboard
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_DASHBOARD)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_DASHBOARD)
fun testGroupLink_dashboard_favoriteLogics() {
val data = setUpGroupAndSignIn()
val user = data.users.values.first()
@@ -91,7 +91,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Test that if no groups has selected as favorite then we display all groups
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_DASHBOARD)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_DASHBOARD)
fun testGroupLink_dashboard_not_selected_displays_all() {
val data = setUpGroupAndSignIn(isFavorite = false)
val user = data.users.values.first()
@@ -107,7 +107,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to file preview opens file - eg: "/groups/:id/files/folder/:id?preview=:id"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_FILES)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_FILES)
fun testGroupLink_filePreview() {
// MBL-13499: This will cause an http request to our mock web server, and http requests from webviews are illegal
@@ -128,7 +128,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to group announcement opens announcement - eg: "/groups/:id/discussion_topics/:id"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_ANNOUNCEMENTS)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_ANNOUNCEMENTS)
fun testGroupLink_announcement() {
setUpGroupAndSignIn()
dashboardPage.selectGroup(group)
@@ -140,7 +140,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to group announcements list opens announcements - eg: "/groups/:id/announcements"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_ANNOUNCEMENTS)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_ANNOUNCEMENTS)
fun testGroupLink_announcementList() {
setUpGroupAndSignIn()
dashboardPage.selectGroup(group)
@@ -150,7 +150,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to group discussion opens discussion - eg: "/groups/:id/discussion_topics/:id"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_DISCUSSIONS)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_DISCUSSIONS)
fun testGroupLink_discussion() {
setUpGroupAndSignIn()
dashboardPage.selectGroup(group)
@@ -161,7 +161,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to group discussion list opens list - eg: "/groups/:id/discussion_topics"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_DISCUSSIONS)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_DISCUSSIONS)
fun testGroupLink_discussionList() {
setUpGroupAndSignIn()
dashboardPage.selectGroup(group)
@@ -171,7 +171,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to group files list opens group files list - eg: "/groups/:id/files"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_FILES)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_FILES)
fun testGroupLink_files() {
setUpGroupAndSignIn()
dashboardPage.selectGroup(group)
@@ -181,7 +181,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to group files folder opens folder - eg: "/groups/:id/files/folder/:id/"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_FILES)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_FILES)
fun testGroupLink_fileFolder() {
setUpGroupAndSignIn()
dashboardPage.selectGroup(group)
@@ -193,7 +193,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to group page list opens pages - eg: "/groups/:id/pages"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_PAGES)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_PAGES)
fun testGroupLink_pagesList() {
setUpGroupAndSignIn()
dashboardPage.selectGroup(group)
@@ -203,7 +203,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to group page opens page - eg: "/groups/:id/pages/:id"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_PAGES)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_PAGES)
fun testGroupLink_Page() {
setUpGroupAndSignIn()
dashboardPage.selectGroup(group)
@@ -216,7 +216,7 @@ class GroupLinksInteractionTest : StudentTest() {
// Link to group people list opens list - eg: "/groups/:id/users"
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_PEOPLE)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, SecondaryFeatureCategory.GROUPS_PEOPLE)
fun testGroupLink_people() {
setUpGroupAndSignIn()
dashboardPage.selectGroup(group)
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 9b8350907f..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
@@ -16,14 +16,18 @@
*/
package com.instructure.student.ui.interaction
-import com.instructure.canvas.espresso.mockCanvas.*
+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.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addAssignment
+import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment
+import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse
+import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.Enrollment
import com.instructure.espresso.page.getStringFromResource
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.R
import com.instructure.student.ui.pages.ElementaryDashboardPage
import com.instructure.student.ui.utils.StudentTest
@@ -37,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 }
@@ -62,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)
@@ -83,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 }
@@ -102,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 }
@@ -123,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()
@@ -156,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 }
@@ -180,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)
@@ -199,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 }
@@ -220,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 }
@@ -230,8 +234,8 @@ class HomeroomInteractionTest : StudentTest() {
val courses = data.courses.values.filter { !it.homeroomCourse }
- data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
- data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ data.addAssignment(courses[0].id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY))
+ data.addAssignment(courses[0].id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY))
goToHomeroomTab(data)
@@ -245,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 }
@@ -255,8 +259,8 @@ class HomeroomInteractionTest : StudentTest() {
val courses = data.courses.values.filter { !it.homeroomCourse }
- val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
- data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ val assignment1 = data.addAssignment(courses[0].id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY))
+ data.addAssignment(courses[0].id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY))
goToHomeroomTab(data)
@@ -268,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 3fd8e2f5ff..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
@@ -16,7 +16,11 @@
package com.instructure.student.ui.interaction
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.StubTablet
+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.addAssignment
import com.instructure.canvas.espresso.mockCanvas.addAssignmentCalendarEvent
@@ -26,10 +30,6 @@ import com.instructure.canvasapi2.models.Assignment
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.ElementaryDashboardPage
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLoginElementary
@@ -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,12 +59,12 @@ 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]
- val assignment = data.addAssignment(courseId = course.id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY))
val assignmentScheduleItem = data.addAssignmentCalendarEvent(course.id, 2.days.fromNow.iso8601, assignment.name!!, assignment.description!!, true, assignment)
goToImportantDatesTab(data)
@@ -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,12 +127,12 @@ 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]
- val assignment = data.addAssignment(courseId = course.id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY))
val assignmentScheduleItem = data.addAssignmentCalendarEvent(course.id, 2.days.fromNow.iso8601, assignment.name!!, assignment.description!!, true, assignment)
goToImportantDatesTab(data)
@@ -147,12 +147,12 @@ 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]
- val assignment = data.addAssignment(courseId = course.id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY))
data.addAssignmentCalendarEvent(course.id, 2.days.fromNow.iso8601, assignment.name!!, assignment.description!!, true, assignment)
val calendarEvent = data.addCourseCalendarEvent(course.id, 2.days.fromNow.iso8601, "Important event", "Important event description", true)
@@ -171,12 +171,12 @@ 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]
- val assignment = data.addAssignment(courseId = course.id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY))
val twoDaysFromNowEvent = data.addAssignmentCalendarEvent(course.id,
2.days.fromNow.iso8601, "Important event two days later", "Important event two days later description", true, assignment)
val threeDaysFromNowEvent = data.addCourseCalendarEvent(course.id,
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/InboxInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt
index ab6d911ab9..bf6fbcdcec 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt
@@ -17,12 +17,24 @@ package com.instructure.student.ui.interaction
import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers
-import com.instructure.canvas.espresso.mockCanvas.*
-import com.instructure.canvasapi2.models.*
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addConversation
+import com.instructure.canvas.espresso.mockCanvas.addConversations
+import com.instructure.canvas.espresso.mockCanvas.addConversationsToCourseMap
+import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
+import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse
+import com.instructure.canvas.espresso.mockCanvas.addSentConversation
+import com.instructure.canvas.espresso.mockCanvas.createBasicConversation
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.Attachment
+import com.instructure.canvasapi2.models.CanvasContextPermission
+import com.instructure.canvasapi2.models.Conversation
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.User
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/LoginInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/LoginInteractionTest.kt
index f70b605a3a..99c1dfdd98 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/LoginInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/LoginInteractionTest.kt
@@ -15,11 +15,11 @@
*/
package com.instructure.student.ui.interaction
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.Stub
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+import com.instructure.canvas.espresso.TestCategory
+import com.instructure.canvas.espresso.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
@@ -30,13 +30,13 @@ class LoginInteractionTest : StudentTest() {
@Stub
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.LOGIN, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.LOGIN, TestCategory.INTERACTION)
fun testLogin_canFindSchool() {
// Should be able to search for and select a school in the "What's your school's name?" page
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.LOGIN, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.LOGIN, TestCategory.INTERACTION)
fun testLogin_qrTutorialPageLoads() {
// Should be able to view and assert page objects on the QR tutorial page
loginLandingPage.clickQRCodeButton()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt
index 20c99a602c..a4af822a17 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt
@@ -18,6 +18,11 @@ package com.instructure.student.ui.interaction
import android.text.Html
import androidx.test.espresso.Espresso
import androidx.test.espresso.web.webdriver.Locator
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addAssignment
import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse
@@ -43,11 +48,6 @@ import com.instructure.canvasapi2.models.Tab
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.SecondaryFeatureCategory
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.R
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
@@ -71,7 +71,7 @@ class ModuleInteractionTest : StudentTest() {
// Tapping an Assignment module item should navigate to that item's detail page
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, false, SecondaryFeatureCategory.MODULES_ASSIGNMENTS)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, SecondaryFeatureCategory.MODULES_ASSIGNMENTS)
fun testModules_launchesIntoAssignment() {
// Basic mock setup
val data = getToCourseModules(studentCount = 1, courseCount = 1)
@@ -81,7 +81,7 @@ class ModuleInteractionTest : StudentTest() {
// Create an assignment and add it as a module item
assignment = data.addAssignment(
courseId = course1.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
data.addItemToModule(
course = course1,
@@ -100,7 +100,7 @@ class ModuleInteractionTest : StudentTest() {
// Tapping a Discussion module item should navigate to that item's detail page
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, false, SecondaryFeatureCategory.MODULES_DISCUSSIONS)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, SecondaryFeatureCategory.MODULES_DISCUSSIONS)
fun testModules_launchesIntoDiscussion() {
// Basic mock setup
val data = getToCourseModules(studentCount = 1, courseCount = 1)
@@ -151,7 +151,7 @@ class ModuleInteractionTest : StudentTest() {
// Tapping an ExternalURL module item should navigate to that item's detail page
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION)
fun testModules_launchesIntoExternalURL() {
// Basic mock setup
val externalUrl = "https://www.google.com"
@@ -176,7 +176,7 @@ class ModuleInteractionTest : StudentTest() {
// Tapping a File module item should navigate to that item's detail page
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, false, SecondaryFeatureCategory.MODULES_FILES)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, SecondaryFeatureCategory.MODULES_FILES)
fun testModules_launchesIntoFile() {
// Basic mock setup
val data = getToCourseModules(studentCount = 1, courseCount = 1)
@@ -210,7 +210,7 @@ class ModuleInteractionTest : StudentTest() {
// Tapping a Page module item should navigate to that item's detail page
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, false, SecondaryFeatureCategory.MODULES_PAGES)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, SecondaryFeatureCategory.MODULES_PAGES)
fun testModules_launchesIntoPage() {
// Basic mock setup
val data = getToCourseModules(studentCount = 1, courseCount = 1)
@@ -251,7 +251,7 @@ class ModuleInteractionTest : StudentTest() {
// Tapping a Quiz module item should navigate to that item's detail page
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, false, SecondaryFeatureCategory.EVENTS_QUIZZES)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, SecondaryFeatureCategory.EVENTS_QUIZZES)
fun testModules_launchesIntoQuiz() {
// Basic mock setup
val data = getToCourseModules(studentCount = 1, courseCount = 1)
@@ -314,7 +314,7 @@ class ModuleInteractionTest : StudentTest() {
// Tapping a module should collapse and hide all of that module's items in the module list
// Tapping a collapsed module should expand it
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION)
fun testModules_modulesExpandAndCollapse() {
// Basic mock setup
val data = getToCourseModules(studentCount = 1, courseCount = 1)
@@ -324,7 +324,7 @@ class ModuleInteractionTest : StudentTest() {
// Create an assignment and add it as a module item
assignment = data.addAssignment(
courseId = course1.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
data.addItemToModule(
course = course1,
@@ -353,7 +353,7 @@ class ModuleInteractionTest : StudentTest() {
// After entering the detail page for a module item, pressing the back button or back arrow should navigate back
// to the module list. This should also work if the detail page is accessed via deep link
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION)
fun testModules_navigateBackToModuleListFromModuleItem() {
// Basic mock setup
val data = getToCourseModules(studentCount = 1, courseCount = 1)
@@ -373,7 +373,7 @@ class ModuleInteractionTest : StudentTest() {
// When viewing the detail page for an item in a module with multiple items, the detail page should have
// 'next' and 'previous' navigation buttons. Clicking these should navigate to the next/previous module items.
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION)
fun testModules_navigateToNextAndPreviousModuleItems() {
// Basic mock setup
val data = getToCourseModules(studentCount = 1, courseCount = 1)
@@ -385,7 +385,7 @@ class ModuleInteractionTest : StudentTest() {
// Create an assignment and add it as a module item
assignment = data.addAssignment(
courseId = course1.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
data.addItemToModule(
course = course1,
@@ -482,7 +482,7 @@ class ModuleInteractionTest : StudentTest() {
// Module can't be accessed unless all prerequisites have been fulfilled
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION)
fun testModules_moduleLockedWithUnfulfilledPrerequisite() {
// Basic mock setup
@@ -501,7 +501,7 @@ class ModuleInteractionTest : StudentTest() {
// And let's add an assignment to the new module
var unavailableAssignment = data.addAssignment(
courseId = course1.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
// Man, this is a bit hokey, but it's what I had to do to get the assignment to show
// up as unavailable in the assignment details page
lockInfo = LockInfo(
@@ -524,7 +524,7 @@ class ModuleInteractionTest : StudentTest() {
// Module can't be accessed until the availability date has passed
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION)
fun testModules_moduleLockedUntilAvailabilityDate() {
// Basic mock setup
val data = getToCourseModules(studentCount = 1, courseCount = 1)
@@ -541,7 +541,7 @@ class ModuleInteractionTest : StudentTest() {
// And let's create an assignment and add it to the "locked" module.
val lockedAssignment = data.addAssignment(
courseId = course1.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
data.addItemToModule(
course = course1,
@@ -558,7 +558,7 @@ class ModuleInteractionTest : StudentTest() {
// Show possible points for assignments in modules if not restricted
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION)
fun testModules_showPossiblePointsIfNotRestricted() {
val data = getToCourseModules(studentCount = 1, courseCount = 1)
val course = data.courses.values.first()
@@ -568,7 +568,7 @@ class ModuleInteractionTest : StudentTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
pointsPossible = 10
)
@@ -585,7 +585,7 @@ class ModuleInteractionTest : StudentTest() {
// Hide possible points for assignments in modules if restricted
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION)
fun testModules_hidePossiblePointsIfRestricted() {
val data = getToCourseModules(studentCount = 1, courseCount = 1)
val course = data.courses.values.first()
@@ -595,7 +595,7 @@ class ModuleInteractionTest : StudentTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
pointsPossible = 10
)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt
index 17ba053fe2..51c5b5dc53 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt
@@ -23,6 +23,10 @@ import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Course
@@ -31,10 +35,6 @@ import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.ContextKeeper
import com.instructure.loginapi.login.model.SignedInUser
import com.instructure.loginapi.login.util.PreviousUsersUtils
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.pandautils.di.NetworkStateProviderModule
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.R
@@ -77,7 +77,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
// Should be able to change the user from the navigation drawer
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.LOGIN, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.LOGIN, TestCategory.INTERACTION)
fun testNavDrawer_changeUser() {
// This test fails on API-28 in FTL due to a "TOO_MANY_REGISTRATIONS" issue on logout.
@@ -123,7 +123,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
// Should be able to log out from the navigation drawer
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.LOGIN, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.LOGIN, TestCategory.INTERACTION)
fun testNavDrawer_logOut() {
// This test fails on API-28 in FTL due to a "TOO_MANY_REGISTRATIONS" issue on logout.
@@ -141,7 +141,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
// Should open a dialog and send a question for the selected course
// (Checks to see that we can fill out the question and the SEND button exists.)
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testHelp_askQuestion() {
signInStudent()
@@ -152,7 +152,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
// Should open the Canvas guides in a WebView
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testHelp_searchCanvasGuides() {
signInStudent()
@@ -164,7 +164,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
// Should send an error report
// (Checks to see that we can fill out an error report and that the SEND button is displayed.)
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testHelp_reportAProblem() {
signInStudent()
@@ -181,7 +181,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
//
// So this is a watered-down test that just checks whether an email app chooser gets displayed.
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testHelp_submitFeatureIdea() {
signInStudent()
@@ -231,7 +231,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
// Should send an intent to open the listing for Student App in the Play Store
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testHelp_shareYourLove() {
signInStudent()
@@ -255,7 +255,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testMenuItemForDefaultStudent() {
signInStudent()
@@ -263,7 +263,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testMenuItemForElementaryStudent() {
signInElementaryStudent()
@@ -271,7 +271,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testOfflineIndicatorDisplayedIfOffline() {
signInStudent()
@@ -281,7 +281,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testOfflineIndicatorNotDisplayedIfOnline() {
signInStudent()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt
index 3a28429766..adde4b4802 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt
@@ -15,16 +15,20 @@
*/
package com.instructure.student.ui.interaction
-import com.instructure.canvas.espresso.mockCanvas.*
+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.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addAssignment
+import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment
+import com.instructure.canvas.espresso.mockCanvas.addSubmissionStreamItem
+import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CourseSettings
import com.instructure.dataseeding.util.ago
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -36,7 +40,7 @@ class NotificationInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testClick_itWorks() {
// Test that push notifications work when you click on them
val data = goToNotifications()
@@ -49,7 +53,7 @@ class NotificationInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testNotificationList_showGradeIfNotRestricted_points() {
val grade = "10.0"
val data = goToNotifications(
@@ -65,7 +69,7 @@ class NotificationInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testNotificationList_showGradeIfNotRestricted_percent() {
val grade = "10%"
val data = goToNotifications(
@@ -81,7 +85,7 @@ class NotificationInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testNotificationList_showGradeIfNotRestricted_letter() {
val grade = "A"
val data = goToNotifications(
@@ -97,7 +101,7 @@ class NotificationInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testNotificationList_showGradeIfNotRestricted_gpa() {
val grade = "GPA"
val data = goToNotifications(
@@ -113,7 +117,7 @@ class NotificationInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testNotificationList_showGradeIfNotRestricted_passFail() {
val grade = "complete"
val data = goToNotifications(
@@ -130,7 +134,7 @@ class NotificationInteractionTest : StudentTest() {
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testNotificationList_showGradeUpdatedIfRestricted_points() {
val grade = "15.0"
val data = goToNotifications(
@@ -146,7 +150,7 @@ class NotificationInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testNotificationList_convertGradeIfRestricted_percent() {
val grade = "50%"
val data = goToNotifications(
@@ -162,7 +166,7 @@ class NotificationInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testNotificationList_showGradeIfRestricted_letter() {
val grade = "A"
val data = goToNotifications(
@@ -178,7 +182,7 @@ class NotificationInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testNotificationList_showGradeIfRestricted_gpa() {
val grade = "GPA"
val data = goToNotifications(
@@ -194,7 +198,7 @@ class NotificationInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testNotificationList_showGradeIfRestricted_passFail() {
val grade = "complete"
val data = goToNotifications(
@@ -210,7 +214,7 @@ class NotificationInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION)
fun testNotificationList_showExcused() {
val data = goToNotifications(
restrictQuantitativeData = true,
@@ -251,7 +255,7 @@ class NotificationInteractionTest : StudentTest() {
repeat(numSubmissions) {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
gradingType = Assignment.gradingTypeToAPIString(gradingType).orEmpty(),
pointsPossible = 20
)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt
index 2be3ec69ac..d429d3b65d 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt
@@ -20,16 +20,16 @@ package com.instructure.student.ui.interaction
import android.text.format.Formatter
import androidx.test.espresso.Espresso
import com.google.android.material.checkbox.MaterialCheckBox
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addFileToCourse
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.Tab
import com.instructure.dataseeding.util.Randomizer
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.pandautils.R
import com.instructure.pandautils.utils.StorageUtils
import com.instructure.student.ui.utils.StudentTest
@@ -47,14 +47,14 @@ class OfflineContentInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun displaysNoCourses() {
goToOfflineContent(createMockCanvas(courseCount = 0))
manageOfflineContentPage.assertDisplaysNoCourses()
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun displaysEmptyCourse() {
val data = createMockCanvas(courseCount = 1, hasTabs = false)
goToOfflineContent(data)
@@ -63,7 +63,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun displaysCoursesCollapsedIfGlobalOfflineContent() {
val data = createMockCanvas()
goToOfflineContent(data)
@@ -72,15 +72,15 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
- fun displaysCourseExpandedIfCourseOfflineContent() {
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
+ fun displaysCourseCollapsedIfCourseOfflineContent() {
val data = createMockCanvas()
goToOfflineContentByCourse(data)
- manageOfflineContentPage.assertDisplaysItemWithExpandedState(data.courses.values.first().name, true)
+ manageOfflineContentPage.assertDisplaysItemWithExpandedState(data.courses.values.first().name, false)
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun displaysCourseTabsAndFiles() {
val data = createMockCanvas(courseCount = 1)
goToOfflineContent(data)
@@ -90,7 +90,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun expandCourse() {
val data = createMockCanvas()
goToOfflineContent(data)
@@ -98,12 +98,11 @@ class OfflineContentInteractionTest : StudentTest() {
manageOfflineContentPage.assertDisplaysItemWithExpandedState(course.name, false)
manageOfflineContentPage.expandCollapseItem(course.name)
manageOfflineContentPage.assertDisplaysItemWithExpandedState(course.name, true)
- manageOfflineContentPage.assertItemDisplayed(data.courseTabs[course.id]!!.first().label!!)
getCourseItemNames(data, course).forEach { manageOfflineContentPage.assertItemDisplayed(it) }
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun selectCourse() {
val data = createMockCanvas()
goToOfflineContent(data)
@@ -115,7 +114,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun selectTab() {
val data = createMockCanvas()
goToOfflineContent(data)
@@ -129,7 +128,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun expandFilesTab() {
val data = createMockCanvas()
goToOfflineContent(data)
@@ -144,7 +143,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun selectFilesTab() {
val data = createMockCanvas()
goToOfflineContent(data)
@@ -161,7 +160,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun selectFile() {
val data = createMockCanvas()
goToOfflineContent(data)
@@ -178,7 +177,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun selectAllFiles() {
val data = createMockCanvas(courseCount = 1)
goToOfflineContent(data)
@@ -197,7 +196,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun selectAllTabs() {
val data = createMockCanvas()
goToOfflineContent(data)
@@ -215,7 +214,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun selectAllToggle() {
val data = createMockCanvas()
goToOfflineContent(data)
@@ -230,7 +229,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun displaysDiscardDialogIfNeeded() {
goToOfflineContent()
Espresso.pressBack()
@@ -241,7 +240,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun displaysWifiOnlySyncDialog() {
val data = createMockCanvas()
val course = data.courses.values.first()
@@ -257,7 +256,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun displaysSyncDialog() {
val data = createMockCanvas()
val course = data.courses.values.first()
@@ -273,7 +272,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun savesChangesOnSync() {
val data = createMockCanvas()
goToOfflineContent(data)
@@ -284,7 +283,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION)
fun calculatesStorageInfoCorrectly() {
val data = createMockCanvas(fileCount = 10, largeFiles = true)
val course = data.courses.values.first()
@@ -353,7 +352,7 @@ class OfflineContentInteractionTest : StudentTest() {
}
private fun getCourseItemNames(data: MockCanvas, course: Course): List {
- return data.courseTabs[course.id]!!.map { it.label!! } + course.name +
+ return listOf(course.name) + data.courseTabs[course.id]!!.map { it.label!! } +
data.folderFiles[data.courseRootFolders[course.id]!!.id]!!.map { it.displayName!! }
}
@@ -368,9 +367,9 @@ class OfflineContentInteractionTest : StudentTest() {
tokenLogin(data.domain, token, student)
dashboardPage.waitForRender()
leftSideNavigationDrawerPage.clickSettingsMenu()
- settingsPage.openOfflineContentPage()
- syncSettingsPage.clickWifiOnlySwitch()
- syncSettingsPage.clickTurnOff()
+ settingsPage.openOfflineSyncSettingsPage()
+ offlineSyncSettingsPage.clickWifiOnlySwitch()
+ offlineSyncSettingsPage.clickTurnOff()
Espresso.pressBack()
Espresso.pressBack()
dashboardPage.openGlobalManageOfflineContentPage()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt
index 5c744e014f..8c61146e4e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt
@@ -21,11 +21,21 @@ import androidx.test.espresso.web.webdriver.DriverAtoms
import androidx.test.espresso.web.webdriver.Locator
import androidx.test.platform.app.InstrumentationRegistry
import com.instructure.annotations.FileCaching.FileCache
-import com.instructure.canvas.espresso.mockCanvas.*
+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.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addAnnotation
+import com.instructure.canvas.espresso.mockCanvas.addAssignment
+import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups
+import com.instructure.canvas.espresso.mockCanvas.addFileToCourse
+import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment
+import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.Attachment
import com.instructure.canvasapi2.models.Tab
-import com.instructure.panda_annotations.*
import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.routeTo
@@ -47,14 +57,14 @@ class PdfInteractionTest : StudentTest() {
private lateinit var attachment: Attachment
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
fun testAnnotations_viewPdfSubmission() {
goToAssignmentPdfSubmission()
submissionDetailsPage.assertFileDisplayed(pdfFileName)
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
fun testAnnotations_viewAndSelectAnnotationsInSubmission() {
goToAssignmentPdfSubmission()
submissionDetailsPage.clickSubmissionContentAtPosition(.5f, .5f)
@@ -62,7 +72,7 @@ class PdfInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
fun testAnnotations_selectAndCommentOnAnnotationWithNoExistingComments() {
val sentCommentContents = "what up dog"
// Configure the comment to be sent in mock Canvas
@@ -75,7 +85,7 @@ class PdfInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
fun testAnnotations_selectAndCommentOnAnnotationWithExistingComments() {
val sentCommentContents = "what up dog"
// Configure the comment to be sent in mock Canvas and the existing comment
@@ -89,7 +99,7 @@ class PdfInteractionTest : StudentTest() {
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION, false, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
fun testAnnotations_openPdfFilesInPSPDFKit() {
// Annotation toolbar icon needs to be present
val data = getToCourse()
@@ -110,7 +120,7 @@ class PdfInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, SecondaryFeatureCategory.SUBMISSIONS_ANNOTATIONS)
fun testAnnotations_openPdfsInPSPDFKitFromLinksInAssignment() {
// Annotation toolbar icon needs to be present, this link is specific to assignment details, as that was the advertised use case
val data = MockCanvas.init(
@@ -143,7 +153,7 @@ class PdfInteractionTest : StudentTest() {
val pdfUrlElementId = "testLinkElement"
val assignmentDescriptionHtml = """pdf baby!!!"""
- val assignment = data.addAssignment(courseId = course.id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD, description = assignmentDescriptionHtml)
+ val assignment = data.addAssignment(courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_UPLOAD), description = assignmentDescriptionHtml)
assignmentListPage.waitForPage()
assignmentListPage.refresh()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt
index effce85116..d3339ff86d 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt
@@ -15,13 +15,13 @@
*/
package com.instructure.student.ui.interaction
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.User
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.routeTo
import com.instructure.student.ui.utils.tokenLogin
@@ -33,7 +33,7 @@ class PeopleInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.PEOPLE, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.PEOPLE, TestCategory.INTERACTION)
fun testClick_openContextCard() {
// Should be able to view all enrolled users and tap on one to open their context card
goToPeopleList()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt
index 3d3bffea00..333abfc1da 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt
@@ -23,15 +23,15 @@ import android.net.Uri
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.Stub
+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.addAssignment
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -70,34 +70,34 @@ class PickerSubmissionUploadInteractionTest : StudentTest() {
@Stub
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
fun testFab_camera() {
}
@Stub
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
fun testFab_galleryPicker() {
}
@Stub
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
fun testFab_filePicker() {
}
@Stub
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
fun testDeleteFile() {
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
fun testSubmit() {
val data = goToSubmissionPicker()
@@ -155,7 +155,7 @@ class PickerSubmissionUploadInteractionTest : StudentTest() {
// Let's set up an assignment that requires an online upload
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_UPLOAD
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_UPLOAD)
)
// Sign in
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt
index a9fcba7951..cb58b48fd2 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt
@@ -2,13 +2,13 @@ package com.instructure.student.ui.interaction
import androidx.test.espresso.Espresso
import androidx.test.rule.GrantPermissionRule
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addUserPermissions
import com.instructure.canvas.espresso.mockCanvas.init
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.R
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
@@ -69,7 +69,7 @@ class ProfileSettingsInteractionTest : StudentTest() {
// Creates a panda avatar, saves it, and verifies that a new panda avatar was saved.
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testProfileSettings_createPandaAvatar() {
val data = MockCanvas.init(studentCount = 1, teacherCount = 1, courseCount = 1, favoriteCourseCount = 1)
val student = data.students[0]
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt
index d35583483c..cd90ead0f5 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt
@@ -15,15 +15,15 @@
*/
package com.instructure.student.ui.interaction
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.CourseSettings
import com.instructure.canvasapi2.models.Quiz
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
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 d4e42a2fcd..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
@@ -16,12 +16,16 @@
*/
package com.instructure.student.ui.interaction
-import com.instructure.canvas.espresso.mockCanvas.*
+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.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment
+import com.instructure.canvas.espresso.mockCanvas.addEnrollment
+import com.instructure.canvas.espresso.mockCanvas.addLTITool
+import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Enrollment
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.ElementaryDashboardPage
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLoginElementary
@@ -34,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)
@@ -63,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)
@@ -91,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)
@@ -113,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)
@@ -148,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)
@@ -171,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)
@@ -197,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)
@@ -221,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 dc0d06eb3b..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
@@ -16,7 +16,11 @@
*/
package com.instructure.student.ui.interaction
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.StubLandscape
+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.addAssignment
import com.instructure.canvas.espresso.mockCanvas.addTodo
@@ -24,15 +28,11 @@ import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.utils.toApiString
import com.instructure.espresso.page.getStringFromResource
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.pandautils.utils.date.DateTimeProvider
import com.instructure.student.R
import com.instructure.student.ui.pages.ElementaryDashboardPage
-import com.instructure.student.ui.utils.di.FakeDateTimeProvider
import com.instructure.student.ui.utils.StudentTest
+import com.instructure.student.ui.utils.di.FakeDateTimeProvider
import com.instructure.student.ui.utils.tokenLoginElementary
import dagger.hilt.android.testing.HiltAndroidTest
import junit.framework.AssertionFailedError
@@ -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)
@@ -83,7 +83,7 @@ class ScheduleInteractionTest : StudentTest() {
courses[0].name = "Course 1"
val currentDate = dateTimeProvider.getCalendar().time.toApiString()
- val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate, name = "Assignment 1")
+ val assignment1 = data.addAssignment(courses[0].id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY), dueAt = currentDate, name = "Assignment 1")
goToScheduleTab(data)
schedulePage.scrollToPosition(10)
@@ -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)
@@ -101,15 +101,15 @@ class ScheduleInteractionTest : StudentTest() {
val courses = data.courses.values.filter { !it.homeroomCourse }
val currentDate = dateTimeProvider.getCalendar().time.toApiString()
- val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate)
+ val assignment1 = data.addAssignment(courses[0].id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY), dueAt = currentDate)
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)
@@ -140,8 +140,8 @@ class ScheduleInteractionTest : StudentTest() {
schedulePage.assertNoScheduleItemDisplayed()
val currentDate = dateTimeProvider.getCalendar().time.toApiString()
- val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate)
- val assignment2 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate)
+ val assignment1 = data.addAssignment(courses[0].id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY), dueAt = currentDate)
+ val assignment2 = data.addAssignment(courses[0].id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY), dueAt = currentDate)
schedulePage.scrollToPosition(0)
schedulePage.refresh()
@@ -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)
@@ -209,7 +209,7 @@ class ScheduleInteractionTest : StudentTest() {
courses[0].name = "Course 1"
val currentDate = dateTimeProvider.getCalendar().time.toApiString()
- val assignment = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate, name = "Assignment 1")
+ val assignment = data.addAssignment(courses[0].id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY), dueAt = currentDate, name = "Assignment 1")
goToScheduleTab(data)
schedulePage.scrollToPosition(9)
@@ -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)
@@ -228,7 +228,7 @@ class ScheduleInteractionTest : StudentTest() {
val courses = data.courses.values.filter { !it.homeroomCourse }
val currentDate = dateTimeProvider.getCalendar().time.toApiString()
- data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate)
+ data.addAssignment(courses[0].id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY), dueAt = currentDate)
goToScheduleTab(data)
schedulePage.scrollToPosition(8)
@@ -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)
@@ -265,7 +265,7 @@ class ScheduleInteractionTest : StudentTest() {
courses[0].name = "Course 1"
val currentDate = dateTimeProvider.getCalendar().time.toApiString()
- val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate, name = "Assignment 1")
+ val assignment1 = data.addAssignment(courses[0].id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY), dueAt = currentDate, name = "Assignment 1")
goToScheduleTab(data)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt
index 4238f38ec8..762fb310e1 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt
@@ -20,15 +20,15 @@ import android.app.Instrumentation
import android.content.Intent
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.StubMultiAPILevel
+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.canvasapi2.models.Course
import com.instructure.canvasapi2.utils.ApiPrefs
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -54,7 +54,7 @@ class SettingsInteractionTest : StudentTest() {
// Should launch an intent to go to our canvas-android github page
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testLegal_showCanvasOnGithub() {
setUpAndSignIn()
@@ -75,7 +75,7 @@ class SettingsInteractionTest : StudentTest() {
// Should display terms of use in a WebView
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testLegal_showTermsOfUse() {
setUpAndSignIn()
@@ -87,7 +87,7 @@ class SettingsInteractionTest : StudentTest() {
// Should display the privacy policy in a WebView
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
@StubMultiAPILevel("Failed API levels = { 28 }", "Somehow the Privacy Policy URL does not load on API lvl 28, but does on other API lvl devices.")
fun testLegal_showPrivacyPolicy() {
setUpAndSignIn()
@@ -96,14 +96,14 @@ class SettingsInteractionTest : StudentTest() {
settingsPage.openLegalPage()
legalPage.openPrivacyPolicy()
canvasWebViewPage.acceptCookiePolicyIfNecessary()
- canvasWebViewPage.checkWebViewURL("https://www.instructure.com/canvas/privacy")
+ canvasWebViewPage.checkWebViewURL("https://www.instructure.com/policies/product-privacy-policy")
}
// Should open a page and have a pairing code that can be refreshed
// (Checks to see that we can refresh and get a new code)
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testPairObserver_refreshCode() {
setUpAndSignIn()
@@ -117,7 +117,7 @@ class SettingsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testOfflineContent_notDisplayedIfFeatureIsDisabled() {
setUpAndSignIn(offlineEnabled = false)
@@ -126,7 +126,7 @@ class SettingsInteractionTest : StudentTest() {
}
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION)
fun testOfflineContent_displayedIfFeatureIsEnabled() {
setUpAndSignIn(offlineEnabled = true)
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 f19c657307..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
@@ -131,7 +132,7 @@ class ShareExtensionInteractionTest : StudentTest() {
val uri = setupFileOnDevice("sample.jpg")
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
- val assignment = data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD)
+ val assignment = data.addAssignment(data.courses.values.first().id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_UPLOAD))
login(student)
device.pressHome()
@@ -182,8 +183,8 @@ class ShareExtensionInteractionTest : StudentTest() {
val uri = setupFileOnDevice("sample.jpg")
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
- data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD)
- val assignment2 = data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD)
+ data.addAssignment(data.courses.values.first().id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_UPLOAD))
+ val assignment2 = data.addAssignment(data.courses.values.first().id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_UPLOAD))
login(student)
device.pressHome()
@@ -233,13 +234,14 @@ class ShareExtensionInteractionTest : StudentTest() {
}
@Test
+ @StubCoverage("Cannot init FileUploadWorker and OfflineSyncWorker")
fun testFileAssignmentSubmission() {
val data = createMockData()
val student = data.students[0]
val uri = setupFileOnDevice("sample.jpg")
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
- data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD)
+ data.addAssignment(data.courses.values.first().id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_UPLOAD))
login(student)
device.pressHome()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt
index e7ed73b4d5..30203de91a 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt
@@ -18,13 +18,24 @@ package com.instructure.student.ui.interaction
import android.os.SystemClock.sleep
import androidx.test.espresso.web.webdriver.Locator
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.Stub
-import com.instructure.canvas.espresso.mockCanvas.*
-import com.instructure.canvasapi2.models.*
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.addAssignment
+import com.instructure.canvas.espresso.mockCanvas.addFileToCourse
+import com.instructure.canvas.espresso.mockCanvas.addRubricToAssignment
+import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.Attachment
+import com.instructure.canvasapi2.models.Author
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.RubricCriterion
+import com.instructure.canvasapi2.models.RubricCriterionRating
+import com.instructure.canvasapi2.models.SubmissionComment
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
@@ -41,12 +52,12 @@ class SubmissionDetailsInteractionTest : StudentTest() {
// Clicking the "Description" button on a rubric criterion item should show a new page with the full description
// Also checks to see that the rubric criterion is displayed correctly, and responds to clicks correctly
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
fun testRubrics_showCriterionDescription() {
val data = getToCourse()
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
pointsPossible = 10
)
@@ -85,13 +96,13 @@ class SubmissionDetailsInteractionTest : StudentTest() {
// Should be able to add a comment on a submission
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
fun testComments_addCommentToSingleAttemptSubmission() {
val data = getToCourse()
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_URL
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_URL)
)
courseBrowserPage.selectAssignments()
@@ -113,7 +124,7 @@ class SubmissionDetailsInteractionTest : StudentTest() {
val data = getToCourse()
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_URL,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_URL),
userSubmitted = true
)
@@ -151,14 +162,14 @@ class SubmissionDetailsInteractionTest : StudentTest() {
// Student can preview an assignment comment attachment
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
fun testComments_previewAttachment() {
val data = getToCourse()
val user = data.users.values.first()
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
// Some html for an attachment
@@ -217,14 +228,14 @@ class SubmissionDetailsInteractionTest : StudentTest() {
@Stub
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
fun testComments_videoCommentPlayback() {
// After recording a video comment, user should be able to view a replay
}
@Stub
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION)
fun testComments_audioCommentPlayback() {
// After recording an audio comment, user should be able to hear an audio playback
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt
index 63a4f61c12..8b9b613563 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt
@@ -15,6 +15,10 @@
*/
package com.instructure.student.ui.interaction
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addAssignment
import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent
@@ -26,10 +30,6 @@ import com.instructure.canvasapi2.models.Tab
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -43,7 +43,7 @@ class SyllabusInteractionTest : StudentTest() {
// Tests that we can display a calendar event from the syllabus/summary,
// and does some verification of the calendar event.
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.SYLLABUS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.SYLLABUS, TestCategory.INTERACTION)
fun testSyllabus_calendarEvent() {
val data = goToSyllabus(eventCount = 1, assignmentCount = 0)
@@ -87,7 +87,7 @@ class SyllabusInteractionTest : StudentTest() {
repeat(assignmentCount) {
data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
dueAt = 2.days.fromNow.iso8601
)
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt
index e81a192070..2a5c54cd5f 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt
@@ -19,10 +19,10 @@ package com.instructure.student.ui.interaction
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.init
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.pandautils.R
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
@@ -35,51 +35,51 @@ class SyncSettingsInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION)
fun testFurtherSettingsDisplayedByDefault() {
goToSyncSettings()
- syncSettingsPage.assertFurtherSettingsIsDisplayed()
+ offlineSyncSettingsPage.assertFurtherSettingsIsDisplayed()
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION)
fun testClickAutoSyncHidesFurtherSettings() {
goToSyncSettings()
- syncSettingsPage.clickAutoSyncSwitch()
- syncSettingsPage.assertFurtherSettingsNotDisplayed()
+ offlineSyncSettingsPage.clickAutoSyncSwitch()
+ offlineSyncSettingsPage.assertFurtherSettingsNotDisplayed()
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION)
fun testChangeFrequency() {
goToSyncSettings()
- syncSettingsPage.assertFrequencyLabelText(R.string.daily)
- syncSettingsPage.clickFrequency()
- syncSettingsPage.clickDialogOption(R.string.weekly)
- syncSettingsPage.assertFrequencyLabelText(R.string.weekly)
+ offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.daily)
+ offlineSyncSettingsPage.openSyncFrequencySettingsDialog()
+ offlineSyncSettingsPage.clickSyncFrequencyDialogOption(R.string.weekly)
+ offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly)
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION)
fun testChangeContentOverWifiOnly() {
goToSyncSettings()
- syncSettingsPage.assertWifiOnlySwitchIsChecked()
- syncSettingsPage.clickWifiOnlySwitch()
- syncSettingsPage.assertDialogDisplayedWithTitle(R.string.syncSettings_wifiConfirmationTitle)
- syncSettingsPage.clickTurnOff()
- syncSettingsPage.assertWifiOnlySwitchIsNotChecked()
+ offlineSyncSettingsPage.assertWifiOnlySwitchIsChecked()
+ offlineSyncSettingsPage.clickWifiOnlySwitch()
+ offlineSyncSettingsPage.assertTurnOffWifiOnlyDialogTexts()
+ offlineSyncSettingsPage.clickTurnOff()
+ offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked()
}
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION)
fun testChangesSavedCorrectly() {
val data = createMockCanvas()
goToSyncSettings(data)
- syncSettingsPage.clickFrequency()
- syncSettingsPage.clickDialogOption(R.string.weekly)
- syncSettingsPage.clickWifiOnlySwitch()
- syncSettingsPage.clickTurnOff()
+ offlineSyncSettingsPage.openSyncFrequencySettingsDialog()
+ offlineSyncSettingsPage.clickSyncFrequencyDialogOption(R.string.weekly)
+ offlineSyncSettingsPage.clickWifiOnlySwitch()
+ offlineSyncSettingsPage.clickTurnOff()
with(activityRule) {
finishActivity()
@@ -88,8 +88,8 @@ class SyncSettingsInteractionTest : StudentTest() {
goToSyncSettings(data)
- syncSettingsPage.assertFrequencyLabelText(R.string.weekly)
- syncSettingsPage.assertWifiOnlySwitchIsNotChecked()
+ offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly)
+ offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked()
}
private fun createMockCanvas(): MockCanvas {
@@ -104,6 +104,6 @@ class SyncSettingsInteractionTest : StudentTest() {
tokenLogin(data.domain, token, student)
dashboardPage.waitForRender()
leftSideNavigationDrawerPage.clickSettingsMenu()
- settingsPage.openOfflineContentPage()
+ settingsPage.openOfflineSyncSettingsPage()
}
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt
index b7129b3e7e..4e0d5abc4a 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt
@@ -16,8 +16,12 @@
package com.instructure.student.ui.interaction
import androidx.test.espresso.Espresso
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.StubLandscape
import com.instructure.canvas.espresso.StubMultiAPILevel
+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.addAssignment
import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse
@@ -28,10 +32,6 @@ import com.instructure.canvasapi2.models.Quiz
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -47,7 +47,7 @@ class TodoInteractionTest : StudentTest() {
// Todo items should be clickable
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.TODOS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.TODOS, TestCategory.INTERACTION)
fun testClick_todoItemClickable() {
val data = goToTodos()
@@ -67,7 +67,7 @@ class TodoInteractionTest : StudentTest() {
@Test
@StubLandscape("Stubbed because on lowres device in landscape mode, the space is too narrow to scroll properly. Will be refactored and running when we changed to non-lowres device on nightly runs.")
@StubMultiAPILevel("Somehow the 'OK' button within chooseFavoriteCourseFilter row is not clickable and not shown on the layout inspector as well.")
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.TODOS, TestCategory.INTERACTION, false)
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.TODOS, TestCategory.INTERACTION)
fun testFilters() {
val data = goToTodos(courseCount = 2, favoriteCourseCount = 1)
val favoriteCourse = data.courses.values.first {course -> course.isFavorite}
@@ -104,7 +104,7 @@ class TodoInteractionTest : StudentTest() {
for(course in data.courses.values) {
assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
dueAt = 1.days.fromNow.iso8601
)
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 80dbaf73ae..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.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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.pandautils.utils.Const
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
@@ -91,7 +93,8 @@ 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
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION, false)
+ @StubCoverage("Cannot init FileUploadWorker and OfflineSyncWorker")
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION)
fun testUpload_deviceFile() {
goToFilePicker()
@@ -120,7 +123,8 @@ class UserFilesInteractionTest : StudentTest() {
// Should be able to upload a file from the camera
// Mocks the result from the expected intent, then uploads it.
@Test
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION, false)
+ @StubCoverage("Cannot init FileUploadWorker and OfflineSyncWorker")
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION)
fun testUpload_fileFromCamera() {
goToFilePicker()
@@ -161,7 +165,8 @@ 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
- @TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION, false)
+ @StubCoverage("Cannot init FileUploadWorker and OfflineSyncWorker")
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION)
fun testUpload_gallery() {
goToFilePicker()
@@ -191,28 +196,28 @@ class UserFilesInteractionTest : StudentTest() {
@Stub
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.FILES, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.COMMON, FeatureCategory.FILES, TestCategory.INTERACTION)
fun testView_previewAudio() {
// Should be able to preview an audio file
}
@Stub
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.FILES, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.COMMON, FeatureCategory.FILES, TestCategory.INTERACTION)
fun testView_previewVideo() {
// Should be able to preview a video file
}
@Stub
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.FILES, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.COMMON, FeatureCategory.FILES, TestCategory.INTERACTION)
fun testView_createDirectory() {
// Should be able to create a directory and upload a file to that directory
}
@Stub
@Test
- @TestMetaData(Priority.COMMON, FeatureCategory.FILES, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.COMMON, FeatureCategory.FILES, TestCategory.INTERACTION)
fun testView_previewImage() {
// Should be able to preview an image file
}
@@ -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/EditDashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt
similarity index 62%
rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt
rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt
index c4bd4d94cf..d14942bad9 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt
@@ -16,25 +16,31 @@
package com.instructure.student.ui.pages
-import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
-import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
-import androidx.test.espresso.matcher.ViewMatchers.withParent
-import androidx.test.espresso.matcher.ViewMatchers.withText
+import android.view.View
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.*
import com.instructure.canvasapi2.models.Course
+import com.instructure.dataseeding.model.CourseApiModel
+import com.instructure.espresso.DoesNotExistAssertion
+import com.instructure.espresso.ViewAlphaAssertion
import com.instructure.espresso.assertDisplayed
import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
import com.instructure.espresso.page.onView
import com.instructure.espresso.page.plus
+import com.instructure.espresso.page.waitForView
import com.instructure.espresso.page.withId
import com.instructure.espresso.page.withText
import com.instructure.espresso.scrollTo
import com.instructure.espresso.swipeUp
import com.instructure.student.R
+import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.anyOf
import org.hamcrest.Matchers.containsString
-class EditDashboardPage : BasePage(R.id.editDashboardPage) {
+class AllCoursesPage : BasePage(R.id.editDashboardPage) {
fun assertCourseDisplayed(course: Course) {
val itemMatcher = allOf(withText(containsString(course.name)), withId(R.id.title))
@@ -102,6 +108,14 @@ class EditDashboardPage : BasePage(R.id.editDashboardPage) {
onView(itemMatcher).assertDisplayed()
}
+ fun assertCourseFavorited(course: CourseApiModel) {
+ val childMatcher = withContentDescription("Remove from dashboard")
+ val itemMatcher = allOf(
+ withContentDescription(containsString("Course ${course.name}, favorite")),
+ hasDescendant(childMatcher))
+ onView(itemMatcher).assertDisplayed()
+ }
+
fun selectAllCourses() {
val childMatcher = withContentDescription("Add all to dashboard")
val itemMatcher = allOf(hasDescendant(withText(R.string.allCoursesCourseHeader)), hasDescendant(childMatcher))
@@ -116,6 +130,17 @@ class EditDashboardPage : BasePage(R.id.editDashboardPage) {
onView(withParent(itemMatcher) + childMatcher).click()
}
+ fun openCourse(courseName: String) {
+ onView(withId(R.id.title) + withText(courseName)).click()
+ }
+
+ //OfflineMethod
+ fun assertSelectUnselectAllButtonNotClickable() {
+ val unselectAllMatcher = withContentDescription("Remove all from dashboard")
+ val selectAllMatcher = withContentDescription("Add all to dashboard")
+ onView(anyOf(unselectAllMatcher, selectAllMatcher)).check(matches(isNotClickable()))
+ }
+
fun assertCourseMassSelectButtonIsDisplayed(someSelected: Boolean) {
if (someSelected) {
@@ -149,4 +174,54 @@ class EditDashboardPage : BasePage(R.id.editDashboardPage) {
onView(withId(R.id.swipeRefreshLayout) + withParent(withId(R.id.editDashboardPage))).swipeUp()
}
+ //OfflineMethod
+ fun assertOfflineNoteDisplayed() {
+ waitForView(withId(R.id.noteTitle) + withText(R.string.allCoursesOfflineNoteTitle)).assertDisplayed()
+ onView(withId(R.id.note) + withText(R.string.allCoursesOfflineNote))
+ }
+
+ //OfflineMethod
+ fun assertOfflineNoteNotDisplayed() {
+ onView(withId(R.id.noteTitle) + withText(R.string.allCoursesOfflineNoteTitle)).check(DoesNotExistAssertion(10))
+ }
+
+ //OfflineMethod
+ fun dismissOfflineNoteBox() {
+ onView(allOf(isAssignableFrom(AppCompatImageView::class.java), hasSibling(withId(R.id.noteTitle)), hasSibling(withId(R.id.note)))).click()
+ onView(withId(R.id.noteTitle) + withText(R.string.allCoursesOfflineNoteTitle)).check(DoesNotExistAssertion(10))
+ }
+
+ //OfflineMethod
+ private fun assertViewAlpha(matcher: Matcher, expectedAlphaValue: Float) {
+ onView(matcher).check(ViewAlphaAssertion(expectedAlphaValue))
+ }
+
+ //OfflineMethod
+ fun assertCourseFavouriteStarAlpha(courseName: String, expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.favoriteButton) + hasSibling(withId(R.id.title) + withText(courseName)), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ fun assertCourseTitleAlpha(courseName: String, expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.title) + withText(courseName), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ fun assertCourseOpenButtonAlpha(courseName: String, expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.openButton) + hasSibling(withId(R.id.title) + withText(courseName)), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ fun assertCourseDetailsAlpha(courseName: String, expectedAlphaValue: Float) {
+ assertCourseFavouriteStarAlpha(courseName, expectedAlphaValue)
+ assertCourseTitleAlpha(courseName, expectedAlphaValue)
+ assertCourseOpenButtonAlpha(courseName, expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ fun assertCourseOfflineSyncButton(courseName: String, visibility: Visibility) {
+ onView(withId(R.id.offlineSyncIcon) + hasSibling(withId(R.id.title) + withText(courseName)))
+ .check(matches(withEffectiveVisibility(visibility)))
+ }
+
}
\ No newline at end of file
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 5164267d7f..b001e424f2 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,18 +20,25 @@ 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
import com.instructure.canvas.espresso.waitForMatcherWithSleeps
import com.instructure.canvasapi2.models.Assignment
+import com.instructure.dataseeding.model.SubmissionType
+import com.instructure.espresso.ModuleItemInteractions
import com.instructure.espresso.OnViewWithId
import com.instructure.espresso.assertContainsText
import com.instructure.espresso.assertDisplayed
@@ -57,8 +64,10 @@ 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) {
+open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage(R.id.assignmentDetailsPage) {
val toolbar by OnViewWithId(R.id.toolbar)
val points by OnViewWithId(R.id.points)
val date by OnViewWithId(R.id.dueDateTextView)
@@ -221,6 +230,86 @@ open class AssignmentDetailsPage : BasePage(R.id.assignmentDetailsPage) {
waitForView(allOf(withId(R.id.attemptTitle), withAncestor(withId(R.id.attemptSpinner)))).assertDisplayed()
waitForView(allOf(withId(R.id.attemptDate), withAncestor(withId(R.id.attemptSpinner)))).assertDisplayed()
}
+
+ fun selectSubmissionType(submissionType: SubmissionType) {
+ val viewMatcher = when (submissionType) {
+ SubmissionType.ONLINE_TEXT_ENTRY -> withId(R.id.submissionEntryText)
+ SubmissionType.ONLINE_UPLOAD -> withId(R.id.submissionEntryFile)
+ SubmissionType.ONLINE_URL -> withId(R.id.submissionEntryWebsite)
+ SubmissionType.MEDIA_RECORDING -> withId(R.id.submissionEntryMedia)
+
+ else -> {withId(R.id.submissionEntryText)}
+ }
+
+ onView(viewMatcher).click()
+ }
+
+ 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/CourseBrowserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt
index d3a9a95924..87cbc0de71 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt
@@ -17,12 +17,15 @@
package com.instructure.student.ui.pages
import android.view.View
+import android.widget.LinearLayout
+import androidx.constraintlayout.widget.ConstraintLayout
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
+import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import com.instructure.canvas.espresso.scrollRecyclerView
import com.instructure.canvas.espresso.withCustomConstraints
@@ -34,11 +37,15 @@ import com.instructure.espresso.WaitForViewWithId
import com.instructure.espresso.assertHasText
import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.plus
+import com.instructure.espresso.scrollTo
import com.instructure.espresso.swipeUp
import com.instructure.pandautils.views.SwipeRefreshLayoutAppBar
import com.instructure.student.R
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.anyOf
+import org.hamcrest.Matchers.not
open class CourseBrowserPage : BasePage(R.id.courseBrowserPage) {
@@ -140,6 +147,15 @@ open class CourseBrowserPage : BasePage(R.id.courseBrowserPage) {
onView(allOf(withText(tabTitle), withId(R.id.label))).check(doesNotExist())
}
+ //OfflineMethod
+ fun assertTabDisabled(tabTitle: String) {
+ onView(allOf(anyOf(isAssignableFrom(LinearLayout::class.java), isAssignableFrom(ConstraintLayout::class.java)), withChild(anyOf(withId(R.id.label), withId(R.id.unsupportedLabel)) + withText(tabTitle)))).scrollTo().check(matches(not(isEnabled())))
+ }
+
+ fun assertTabEnabled(tabTitle: String) {
+ onView(allOf(anyOf(isAssignableFrom(LinearLayout::class.java), isAssignableFrom(ConstraintLayout::class.java)), withChild(anyOf(withId(R.id.label), withId(R.id.unsupportedLabel)) + withText(tabTitle)))).scrollTo().check(matches(isEnabled()))
+ }
+
// Minimizes toolbar if it is not already minimized
private fun minimizeToolbar() {
try {
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 78121a4c7e..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
@@ -38,10 +37,12 @@ import com.instructure.canvasapi2.models.Group
import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.GroupApiModel
import com.instructure.espresso.*
+import com.instructure.espresso.matchers.WaitForViewMatcher.waitForViewToBeCompletelyDisplayed
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
@@ -146,6 +147,10 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
onView(hamburgerButtonMatcher).waitForCheck(matches(isDisplayed()))
}
+ fun openLeftSideMenu() {
+ onView(hamburgerButtonMatcher).click()
+ }
+
private fun scrollAndAssertDisplayed(matcher: Matcher) {
// Arggghhh... This scrolling logic on the recycler view is really unreliable and seems
// to fail for nonsensical reasons. For now, "scrollAndAssertDisplayed"" is just going to
@@ -255,20 +260,24 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
}
fun switchCourseView() {
- Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext)
+ clickDashboardGlobalOverflowButton()
onView(withText(containsString("Switch to")))
.perform(click());
}
- //OfflineMethod
fun openGlobalManageOfflineContentPage() {
- Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext)
- onView(withText(containsString("Manage Offline Content")))
+ clickDashboardGlobalOverflowButton()
+ onView(withText(containsString("Manage Offline Content")))
.perform(click());
}
- fun clickEditDashboard() {
- onView(withId(R.id.editDashboardTextView)).scrollTo().click()
+ private fun clickDashboardGlobalOverflowButton() {
+ waitForViewToBeCompletelyDisplayed(withContentDescription("More options") + withAncestor(R.id.toolbar))
+ Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext)
+ }
+
+ fun openAllCoursesPage() {
+ waitForView(withId(R.id.editDashboardTextView)).scrollTo().click()
}
fun assertCourseNotDisplayed(course: CourseApiModel) {
@@ -304,12 +313,16 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
}
fun clickCourseOverflowMenu(courseTitle: String, menuTitle: String) {
+ clickOnCourseOverflowButton(courseTitle)
+ waitForView(withId(R.id.title) + withText(menuTitle)).click()
+ }
+
+ fun clickOnCourseOverflowButton(courseTitle: String) {
val courseOverflowMatcher = withId(R.id.overflow) + withAncestor(
withId(R.id.cardView)
+ withDescendant(withId(R.id.titleTextView) + withText(courseTitle))
)
waitForView(courseOverflowMatcher).scrollTo().click()
- waitForView(withId(R.id.title) + withText(menuTitle)).click()
}
fun assertCourseGrade(courseName: String, courseGrade: String) {
@@ -345,6 +358,31 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
onView(withId(R.id.offlineIndicator)).check(matches(withEffectiveVisibility(Visibility.GONE)))
}
+ //OfflineMethod
+ fun waitForOfflineIndicatorNotDisplayed() {
+ assertDisplaysCourses()
+ retry(times = 5, delay = 2000) {
+ assertOfflineIndicatorNotDisplayed()
+ }
+ }
+
+ //OfflineMethod
+ 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)))
@@ -357,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
@@ -379,6 +418,14 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
fun waitForSyncProgressStartingNotificationToDisappear() {
ViewUtils.waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_syncingOfflineContent), 30)
}
+
+ //OfflineMethod
+ fun assertBottomMenusAreDisabled() {
+ onView(withId(R.id.bottomNavigationCalendar)).check(matches(isNotEnabled()))
+ onView(withId(R.id.bottomNavigationToDo)).check(matches(isNotEnabled()))
+ onView(withId(R.id.bottomNavigationNotifications)).check(matches(isNotEnabled()))
+ onView(withId(R.id.bottomNavigationInbox)).check(matches(isNotEnabled()))
+ }
}
/**
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..63bc20c3ff 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
@@ -37,6 +36,7 @@ import com.instructure.canvas.espresso.withCustomConstraints
import com.instructure.canvas.espresso.withElementRepeat
import com.instructure.canvasapi2.models.DiscussionEntry
import com.instructure.canvasapi2.models.DiscussionTopicHeader
+import com.instructure.espresso.ModuleItemInteractions
import com.instructure.espresso.OnViewWithId
import com.instructure.espresso.assertDisplayed
import com.instructure.espresso.assertGone
@@ -47,6 +47,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
@@ -55,7 +57,7 @@ import org.hamcrest.Matchers.containsString
import org.junit.Assert.assertTrue
-class DiscussionDetailsPage : BasePage(R.id.discussionDetailsPage) {
+class DiscussionDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage(R.id.discussionDetailsPage) {
private val discussionTopicTitle by OnViewWithId(R.id.discussionTopicTitle)
private val replyButton by OnViewWithId(R.id.replyToDiscussionTopic)
@@ -91,7 +93,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 +120,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 +320,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/FileListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt
index 1a5b4edd07..786d1e2406 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt
@@ -20,9 +20,7 @@ import androidx.appcompat.widget.AppCompatButton
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeDown
-import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
-import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.*
import com.instructure.canvas.espresso.containsTextCaseInsensitive
import com.instructure.canvas.espresso.scrollRecyclerView
@@ -58,6 +56,11 @@ class FileListPage(val searchable: Searchable) : BasePage(R.id.fileListPage) {
waitForView(matcher).scrollTo().assertDisplayed()
}
+ fun assertSearchItemDisplayed(itemName: String) {
+ val matcher = allOf(withId(R.id.fileName), withAncestor(R.id.fileSearchRecyclerView), withText(itemName))
+ waitForView(matcher).scrollTo().assertDisplayed()
+ }
+
fun assertItemNotDisplayed(itemName: String) {
val matcher = allOf(withId(R.id.fileName), withText(itemName))
onView(matcher).assertNotDisplayed()
@@ -127,14 +130,14 @@ class FileListPage(val searchable: Searchable) : BasePage(R.id.fileListPage) {
fun assertSearchResultCount(expectedCount: Int) {
Thread.sleep(2000)
onView(withId(R.id.fileSearchRecyclerView) + withAncestor(R.id.container)).check(
- ViewAssertions.matches(ViewMatchers.hasChildCount(expectedCount))
+ matches(hasChildCount(expectedCount))
)
}
fun assertFileListCount(expectedCount: Int) {
Thread.sleep(2000)
- onView(withId(R.id.listView) + withAncestor(R.id.container)).check(
- ViewAssertions.matches(ViewMatchers.hasChildCount(expectedCount))
+ onView(withId(R.id.listView) + withAncestor(R.id.fileListPage)).check(
+ matches(hasChildCount(expectedCount))
)
}
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/GoToQuizPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GoToQuizPage.kt
new file mode 100644
index 0000000000..9d6cc3af2c
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GoToQuizPage.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.pages
+
+import com.instructure.espresso.ModuleItemInteractions
+import com.instructure.espresso.OnViewWithText
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.click
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.plus
+import com.instructure.espresso.page.withAncestor
+import com.instructure.espresso.page.withId
+import com.instructure.espresso.page.withText
+import com.instructure.student.R
+
+class GoToQuizPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage() {
+
+ private val goToQuizButton by OnViewWithText(R.string.goToQuiz)
+
+ fun clickGoToQuizButton() {
+ goToQuizButton.click()
+ }
+
+ fun assertQuizTitle(expectedTitle: String) {
+ onView(withId(R.id.quizTitle) + withText(expectedTitle) + withAncestor(R.id.quizInfoContainer)).assertDisplayed()
+ }
+}
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 47ac688d03..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
@@ -145,7 +145,6 @@ class InboxPage : BasePage(R.id.inboxPage) {
hasSibling(allOf(withId(R.id.subjectView), withText(subject))))
waitForMatcherWithRefreshes(matcher) // May need to refresh before the star shows up
onView(matcher).scrollTo().assertDisplayed()
-
}
fun assertConversationNotStarred(subject: String) {
@@ -154,9 +153,7 @@ class InboxPage : BasePage(R.id.inboxPage) {
hasSibling(withId(R.id.userName)),
hasSibling(withId(R.id.date)),
hasSibling(allOf(withId(R.id.subjectView), withText(subject))))
- waitForMatcherWithRefreshes(matcher) // May need to refresh before the star shows up
onView(matcher).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
-
}
fun assertUnreadMarkerVisibility(conversation: Conversation, visibility: ViewMatchers.Visibility) {
@@ -192,7 +189,7 @@ class InboxPage : BasePage(R.id.inboxPage) {
}
}
- fun assertInboxEmpty() {
+ fun assertInboxEmpty() {
waitForView(withId(R.id.emptyInboxView)).assertDisplayed()
}
@@ -303,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/LeftSideNavigationDrawerPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt
index da39b4cefe..0fa4f1c98e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt
@@ -12,8 +12,22 @@ import com.instructure.canvas.espresso.CanvasTest
import com.instructure.canvas.espresso.waitForMatcherWithSleeps
import com.instructure.canvasapi2.models.User
import com.instructure.dataseeding.model.CanvasUserApiModel
-import com.instructure.espresso.*
-import com.instructure.espresso.page.*
+import com.instructure.espresso.OnViewWithContentDescription
+import com.instructure.espresso.OnViewWithId
+import com.instructure.espresso.ViewAlphaAssertion
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.assertNotDisplayed
+import com.instructure.espresso.click
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.onViewWithId
+import com.instructure.espresso.page.onViewWithText
+import com.instructure.espresso.page.waitForView
+import com.instructure.espresso.page.waitForViewWithId
+import com.instructure.espresso.page.withId
+import com.instructure.espresso.scrollTo
+import com.instructure.espresso.swipeDown
+import com.instructure.espresso.swipeUp
import com.instructure.student.R
import org.hamcrest.CoreMatchers
import org.hamcrest.Matcher
@@ -86,6 +100,10 @@ class LeftSideNavigationDrawerPage : BasePage() {
clickMenu(R.id.navigationDrawerItem_bookmarks)
}
+ fun clickStudioMenu() {
+ clickMenu(R.id.navigationDrawerItem_studio)
+ }
+
fun clickSettingsMenu() {
clickMenu(R.id.navigationDrawerSettings)
}
@@ -181,6 +199,73 @@ class LeftSideNavigationDrawerPage : BasePage() {
logoutButton.assertDisplayed()
}
+ //OfflineMethod
+ private fun assertViewAlpha(matcher: Matcher, expectedAlphaValue: Float) {
+ onView(matcher).check(ViewAlphaAssertion(expectedAlphaValue))
+ }
+
+ //OfflineMethod
+ private fun assertFilesMenuAlphaValue(expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.navigationDrawerItem_files), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ private fun assertBookmarksMenuAlphaValue(expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.navigationDrawerItem_bookmarks), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ private fun assertStudioMenuAlphaValue(expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.navigationDrawerItem_studio), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ private fun assertSettingsMenuAlphaValue(expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.navigationDrawerSettings), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ private fun assertShowGradesMenuAlphaValue(expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.navigationDrawerItem_showGrades), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ private fun assertColorOverlayMenuAlphaValue(expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.navigationDrawerItem_colorOverlay), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ private fun assertHelpMenuAlphaValue(expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.navigationDrawerItem_help), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ private fun assertChangeUserMenuAlphaValue(expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.navigationDrawerItem_changeUser), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ private fun assertLogoutMenuAlphaValue(expectedAlphaValue: Float) {
+ assertViewAlpha(withId(R.id.navigationDrawerItem_logout), expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ fun assertOfflineDisabledMenus(expectedAlphaValue: Float) {
+ assertFilesMenuAlphaValue(expectedAlphaValue)
+ assertBookmarksMenuAlphaValue(expectedAlphaValue)
+ assertStudioMenuAlphaValue(expectedAlphaValue)
+ assertColorOverlayMenuAlphaValue(expectedAlphaValue)
+ assertHelpMenuAlphaValue(expectedAlphaValue)
+ }
+
+ //OfflineMethod
+ fun assertOfflineEnabledMenus(expectedAlphaValue: Float) {
+ assertSettingsMenuAlphaValue(expectedAlphaValue)
+ assertShowGradesMenuAlphaValue(expectedAlphaValue)
+ assertChangeUserMenuAlphaValue(expectedAlphaValue)
+ assertLogoutMenuAlphaValue(expectedAlphaValue)
+ }
+
/**
* Custom ViewAction to set a SwitchCompat to the desired on/off position
* [position]: true -> "on", false -> "off"
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageDetailsPage.kt
new file mode 100644
index 0000000000..81b06aaf67
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageDetailsPage.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.pages
+
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
+import androidx.test.espresso.web.model.Atoms
+import androidx.test.espresso.web.sugar.Web.onWebView
+import com.instructure.espresso.ModuleItemInteractions
+import com.instructure.espresso.page.BasePage
+import com.instructure.student.R
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.containsString
+
+class PageDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage() {
+
+ fun webAssertPageUrl(pageUrl: String) {
+ onWebView(allOf(withId(R.id.contentWebView), isDisplayed()))
+ .check(webMatches(Atoms.getCurrentUrl(), containsString(pageUrl)))
+ }
+
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PersonDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PersonDetailsPage.kt
index f5458663e5..6a73ea9746 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PersonDetailsPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PersonDetailsPage.kt
@@ -16,12 +16,18 @@
*/
package com.instructure.student.ui.pages
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import com.instructure.canvasapi2.models.User
import com.instructure.espresso.OnViewWithId
import com.instructure.espresso.assertContainsText
import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.withId
import com.instructure.student.R
+import org.hamcrest.Matchers
class PersonDetailsPage: BasePage(R.id.clickContainer) {
@@ -37,4 +43,9 @@ class PersonDetailsPage: BasePage(R.id.clickContainer) {
fun assertIsPerson(user: User) {
userName.assertContainsText(user.name)
}
+
+ //OfflineMethod
+ fun assertComposeMessageIcon(visibility: ViewMatchers.Visibility) {
+ onView(Matchers.allOf(withId(R.id.compose))).check(matches(withEffectiveVisibility(visibility)))
+ }
}
\ 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/SettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt
index 8f40d0d703..0792369df0 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt
@@ -16,12 +16,22 @@
*/
package com.instructure.student.ui.pages
-import com.instructure.espresso.*
-import com.instructure.espresso.page.*
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import com.instructure.espresso.OnViewWithId
+import com.instructure.espresso.TextViewColorAssertion
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.assertNotDisplayed
+import com.instructure.espresso.click
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.plus
+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.student.R
class SettingsPage : BasePage(R.id.settingsFragment) {
- private val toolbar by OnViewWithId(R.id.toolbar)
private val profileSettingLabel by OnViewWithId(R.id.profileSettings)
private val accountPreferencesLabel by OnViewWithId(R.id.accountPreferences)
private val pushNotificationsLabel by OnViewWithId(R.id.pushNotifications)
@@ -81,15 +91,30 @@ class SettingsPage : BasePage(R.id.settingsFragment) {
appThemeStatus.check(TextViewColorAssertion(expectedTextColor))
}
- fun openOfflineContentPage() {
+ //OfflineMethod
+ fun openOfflineSyncSettingsPage() {
offlineContent.scrollTo().click()
}
+ //OfflineMethod
fun assertOfflineContentDisplayed() {
offlineContent.scrollTo().assertDisplayed()
}
+ //OfflineMethod
fun assertOfflineContentNotDisplayed() {
offlineContent.assertNotDisplayed()
}
+
+ //OfflineMethod
+ fun assertOfflineContentTitle() {
+ onView(withId(R.id.offlineContentTitle) + withText(R.string.offlineContent)).assertDisplayed()
+ }
+
+ //OfflineMethod
+ fun assertOfflineSyncSettingsStatus(expectedStatus: Int) {
+ onView(withId(R.id.offlineSyncSettingsStatus) + withText(expectedStatus) + withParent(R.id.offlineSyncSettingsContainer) +
+ hasSibling(withId(R.id.offlineSyncSettingsTitle) + withText(R.string.offlineSyncSettingsTitle))).assertDisplayed()
+ }
+
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt
deleted file mode 100644
index a7f2db9f3b..0000000000
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2023 - 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.ui.pages
-
-import androidx.test.espresso.assertion.ViewAssertions.matches
-import androidx.test.espresso.matcher.ViewMatchers.isChecked
-import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
-import com.instructure.espresso.*
-import com.instructure.espresso.page.BasePage
-import com.instructure.espresso.page.onViewWithText
-import com.instructure.pandautils.R
-
-class SyncSettingsPage : BasePage(R.id.syncSettingsPage) {
-
- private val toolbar by OnViewWithId(R.id.toolbar)
- private val autoSyncSwitch by OnViewWithId(R.id.autoSyncSwitch)
- private val furtherSettings by OnViewWithId(R.id.furtherSettings)
- private val syncFrequencyLabel by OnViewWithId(R.id.syncFrequencyLabel)
- private val wifiOnlySwitch by OnViewWithId(R.id.wifiOnlySwitch)
-
- fun clickAutoSyncSwitch() {
- autoSyncSwitch.click()
- }
-
- fun clickFrequency() {
- syncFrequencyLabel.click()
- }
-
- fun clickDialogOption(stringResId: Int) {
- onViewWithText(stringResId).click()
- }
-
- fun clickWifiOnlySwitch() {
- wifiOnlySwitch.click()
- }
-
- fun clickTurnOff() {
- onViewWithText(R.string.syncSettings_wifiConfirmationPositiveButton).click()
- }
-
- fun assertFurtherSettingsIsDisplayed() {
- furtherSettings.assertDisplayed()
- }
-
- fun assertFurtherSettingsNotDisplayed() {
- furtherSettings.assertNotDisplayed()
- }
-
- fun assertFrequencyLabelText(expected: Int) {
- syncFrequencyLabel.assertHasText(expected)
- }
-
- fun assertWifiOnlySwitchIsChecked() {
- wifiOnlySwitch.check(matches(isChecked()))
- }
-
- fun assertWifiOnlySwitchIsNotChecked() {
- wifiOnlySwitch.check(matches(isNotChecked()))
- }
-
- fun assertDialogDisplayedWithTitle(title: Int) {
- onViewWithText(title).assertDisplayed()
- }
-}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt
index 87d65a84ae..a64ec38b56 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt
@@ -17,16 +17,36 @@
package com.instructure.student.ui.pages.offline
+import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers.*
import com.instructure.canvas.espresso.containsTextCaseInsensitive
import com.instructure.canvas.espresso.hasCheckedState
import com.instructure.canvas.espresso.withRotation
-import com.instructure.espresso.*
+import com.instructure.espresso.ConstraintLayoutItemCountAssertion
+import com.instructure.espresso.ConstraintLayoutItemCountAssertionWithMatcher
+import com.instructure.espresso.DoesNotExistAssertion
+import com.instructure.espresso.OnViewWithId
+import com.instructure.espresso.WaitForViewWithId
import com.instructure.espresso.actions.ForceClick
-import com.instructure.espresso.page.*
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.click
+import com.instructure.espresso.matchers.WaitForViewMatcher
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.plus
+import com.instructure.espresso.page.waitForView
+import com.instructure.espresso.page.withAncestor
+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.pandautils.R
+import com.instructure.pandautils.binding.BindableViewHolder
import org.hamcrest.CoreMatchers.allOf
class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) {
@@ -34,43 +54,37 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) {
private val syncButton by OnViewWithId(R.id.syncButton)
private val storageInfoContainer by WaitForViewWithId(R.id.storageInfoContainer)
- //OfflineMethod
fun changeItemSelectionState(itemName: String) {
+ onView(withId(R.id.offlineContentRecyclerView))
+ .perform(RecyclerViewActions.scrollTo(hasDescendant(withText(itemName))))
onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().click()
}
- //OfflineMethod
fun expandCollapseItem(itemName: String) {
- onView(withId(R.id.arrow) + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().perform(ForceClick())
+ onView(withId(R.id.arrow) + withEffectiveVisibility(Visibility.VISIBLE) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().perform(ForceClick())
}
- //OfflineMethod
fun expandCollapseFiles() {
expandCollapseItem("Files")
}
- //OfflineMethod
fun clickOnSyncButton() {
syncButton.click()
}
- //OfflineMethod
fun clickOnSyncButtonAndConfirm() {
clickOnSyncButton()
confirmSync()
}
- //OfflineMethod
private fun confirmSync() {
waitForView(withText("Sync") + withAncestor(R.id.buttonPanel)).click()
}
- //OfflineMethod
fun confirmDiscardChanges() {
waitForView(withText("Discard") + withAncestor(R.id.buttonPanel)).click()
}
- //OfflineMethod
fun assertStorageInfoDetails() {
onView(withId(R.id.storageLabel) + withText(R.string.offline_content_storage)).assertDisplayed()
onView(withId(R.id.storageInfo) + containsTextCaseInsensitive("Used")).assertDisplayed()
@@ -80,85 +94,84 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) {
onView(withId(R.id.remainingLabel) + withText(R.string.offline_content_remaining)).assertDisplayed()
}
- //OfflineMethod
fun assertSelectButtonText(selectAll: Boolean) {
- if(selectAll) waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).assertDisplayed()
+ if (selectAll) waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).assertDisplayed()
else waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_deselect_all)).assertDisplayed()
}
- //OfflineMethod
fun clickOnSelectAllButton() {
waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).click()
}
- //OfflineMethod
fun clickOnDeselectAllButton() {
waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_deselect_all)).click()
}
- //OfflineMethod
fun assertCourseCountWithMatcher(expectedCount: Int) {
- ConstraintLayoutItemCountAssertionWithMatcher((allOf(withId(R.id.arrow), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))), expectedCount)
+ ConstraintLayoutItemCountAssertionWithMatcher((allOf(withId(R.id.arrow), withEffectiveVisibility(Visibility.VISIBLE))), expectedCount)
}
- //OfflineMethod
fun assertCourseCount(expectedCount: Int) {
- onView((allOf(withId(R.id.arrow), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))).check(ConstraintLayoutItemCountAssertion(expectedCount))
+ onView((allOf(withId(R.id.arrow), withEffectiveVisibility(Visibility.VISIBLE)))).check(ConstraintLayoutItemCountAssertion(expectedCount))
}
- //OfflineMethod
fun assertToolbarTexts(courseName: String) {
onView(withText(courseName) + withParent(R.id.toolbar) + withAncestor(R.id.manageOfflineContentPage)).assertDisplayed()
onView(withText(R.string.offline_content_toolbar_title) + withParent(R.id.toolbar) + withAncestor(R.id.manageOfflineContentPage)).assertDisplayed()
}
- //OfflineMethod
fun assertCheckedStateOfItem(itemName: String, state: Int) {
- onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName)) + hasCheckedState(state)).scrollTo().assertDisplayed()
+ val matcher = withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName)) + hasCheckedState(state)
+ onView(withId(R.id.offlineContentRecyclerView))
+ .perform(RecyclerViewActions.scrollTo(hasDescendant(withText(itemName))))
+ onView(matcher).scrollTo().assertDisplayed()
}
- //OfflineMethod
fun waitForItemDisappear(itemName: String) {
- onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).check(DoesNotExistAssertion(5))
+ waitForView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).check(DoesNotExistAssertion(5))
}
- //OfflineMethod
fun assertDisplaysNoCourses() {
- onView(withText(R.string.offline_content_empty_message)).assertDisplayed()
+ Espresso.onView(ViewMatchers.withText(R.string.offline_content_empty_message)).assertDisplayed()
}
- //OfflineMethod
fun assertDisplaysEmptyCourse() {
- onView(withText(R.string.offline_content_empty_course_message)).assertDisplayed()
+ Espresso.onView(ViewMatchers.withText(R.string.offline_content_empty_course_message)).scrollTo().assertDisplayed()
}
- //OfflineMethod
fun assertDisplaysItemWithExpandedState(title: String, expanded: Boolean) {
- onView(withId(R.id.arrow)
+ Espresso.onView(
+ ViewMatchers.withId(R.id.arrow)
+ withRotation(if (expanded) 180f else 0f)
- + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)
- + hasSibling(withId(R.id.title) + withText(title))
+ + withEffectiveVisibility(Visibility.VISIBLE)
+ + hasSibling(ViewMatchers.withId(R.id.title) + ViewMatchers.withText(title))
).scrollTo().assertDisplayed()
}
- //OfflineMethod
fun assertItemDisplayed(title: String) {
- onView(withId(R.id.title) + withText(title)).scrollTo().assertDisplayed()
+ val matcher = withId(R.id.title) + withText(title)
+ onView(withId(R.id.offlineContentRecyclerView))
+ .perform(RecyclerViewActions.scrollTo(hasDescendant(matcher)))
+ onView(matcher).scrollTo().assertDisplayed()
}
- //OfflineMethod
fun assertDiscardDialogDisplayed() {
- waitForView(withText(R.string.offline_content_discard_dialog_title)).assertDisplayed()
+ WaitForViewMatcher.waitForView(ViewMatchers.withText(R.string.offline_content_discard_dialog_title))
+ .assertDisplayed()
}
- //OfflineMethod
fun assertSyncDialogDisplayed(text: String) {
- waitForView(withText(text)).assertDisplayed()
+ WaitForViewMatcher.waitForView(ViewMatchers.withText(text)).assertDisplayed()
}
- //OfflineMethod
fun assertStorageInfoText(storageInfoText: String) {
- onView(withId(R.id.storageInfo) + withText(storageInfoText)).assertDisplayed()
+ Espresso.onView(
+ ViewMatchers.withId(R.id.storageInfo) + ViewMatchers.withText(
+ storageInfoText
+ )
+ ).assertDisplayed()
}
}
+
+
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt
new file mode 100644
index 0000000000..d27445850e
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 - 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.ui.pages.offline
+
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isChecked
+import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
+import com.instructure.espresso.OnViewWithId
+import com.instructure.espresso.assertDisplayed
+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.onView
+import com.instructure.espresso.page.onViewWithText
+import com.instructure.espresso.page.plus
+import com.instructure.espresso.page.waitForView
+import com.instructure.espresso.page.withAncestor
+import com.instructure.espresso.page.withId
+import com.instructure.espresso.page.withParent
+import com.instructure.espresso.page.withText
+import com.instructure.pandautils.R
+
+class OfflineSyncSettingsPage : BasePage(R.id.syncSettingsPage) {
+
+ private val toolbar by OnViewWithId(R.id.toolbar)
+ private val autoSyncSwitch by OnViewWithId(R.id.autoSyncSwitch)
+ private val furtherSettings by OnViewWithId(R.id.furtherSettings)
+ private val syncFrequencyLabel by OnViewWithId(R.id.syncFrequencyLabel)
+ private val wifiOnlySwitch by OnViewWithId(R.id.wifiOnlySwitch)
+
+ fun clickAutoSyncSwitch() {
+ autoSyncSwitch.click()
+ }
+
+ fun openSyncFrequencySettingsDialog() {
+ syncFrequencyLabel.click()
+ }
+
+ fun clickSyncFrequencyDialogOption(stringResId: Int) {
+ onView(withText(stringResId) + withParent(R.id.select_dialog_listview)).click()
+ }
+
+ fun clickWifiOnlySwitch() {
+ wifiOnlySwitch.click()
+ }
+
+ fun clickTurnOff() {
+ onViewWithText(R.string.syncSettings_wifiConfirmationPositiveButton).click()
+ }
+
+ fun assertTurnOffWifiOnlyDialogTexts() {
+ waitForView(withId(R.id.alertTitle) + withText(R.string.syncSettings_wifiConfirmationTitle)).assertDisplayed()
+ waitForView(withText(R.string.syncSettings_wifiConfirmationPositiveButton) + withAncestor(R.id.buttonPanel)).assertDisplayed()
+ onView(withText(R.string.synySettings_wifiConfirmationMessage)).assertDisplayed()
+ }
+
+ fun assertFurtherSettingsIsDisplayed() {
+ furtherSettings.assertDisplayed()
+ }
+
+ fun assertFurtherSettingsNotDisplayed() {
+ furtherSettings.assertNotDisplayed()
+ }
+
+ fun assertSyncFrequencyLabelText(expected: Int) {
+ syncFrequencyLabel.assertHasText(expected)
+ }
+
+ fun assertSyncFrequencyTitleText() {
+ onView(withText(R.string.syncSettings_syncFrequencyTitle) + withParent(R.id.syncFrequencyContainer)).assertDisplayed()
+ }
+
+ fun assertWifiOnlySwitchIsChecked() {
+ wifiOnlySwitch.check(matches(isChecked()))
+ }
+
+ fun assertWifiOnlySwitchIsNotChecked() {
+ wifiOnlySwitch.check(matches(isNotChecked()))
+ }
+
+ fun assertAutoSyncSwitchIsChecked() {
+ autoSyncSwitch.check(matches(isChecked()))
+ }
+
+ fun assertAutoSyncSwitchIsNotChecked() {
+ autoSyncSwitch.check(matches(isNotChecked()))
+ }
+
+ fun assertDialogDisplayedWithTitle(title: String) {
+ onViewWithText(title).assertDisplayed()
+ }
+
+ fun assertSyncSettingsToolbarTitle() {
+ onView(withText(com.instructure.student.R.string.syncSettings_toolbarTitle) + withParent(withId(
+ com.instructure.student.R.id.toolbar) + withAncestor(com.instructure.student.R.id.syncSettingsPage))).assertDisplayed()
+ }
+
+ fun assertSyncSettingsPageDescriptions() {
+ onView(withText(R.string.syncSettings_autoContentSyncDescription)).assertDisplayed()
+ onView(withText(R.string.syncSettings_syncFrequencyDescription)).assertDisplayed()
+ onView(withText(R.string.syncSettings_wifiOnlyDescription)).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/renderTests/DiscussionSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt
index aa025108c2..b76a80cf54 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt
@@ -15,10 +15,10 @@
*/
package com.instructure.student.ui.renderTests
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.DiscussionSubmissionViewFragment
import com.instructure.student.ui.pages.renderPages.DiscussionSubmissionViewRenderPage
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt
index bc0a772bb7..b1a7a71639 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt
@@ -18,16 +18,16 @@ package com.instructure.student.ui.renderTests
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.ext.junit.runners.AndroidJUnit4
+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.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.Course
import com.instructure.espresso.assertDisplayed
import com.instructure.espresso.assertHasText
import com.instructure.espresso.assertNotDisplayed
import com.instructure.espresso.assertVisible
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.R
import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt
index 7f1ec29f08..c3ddebf0ee 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt
@@ -18,11 +18,15 @@ package com.instructure.student.ui.renderTests
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
import androidx.test.ext.junit.runners.AndroidJUnit4
+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.models.Assignment
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Submission
import com.instructure.canvasapi2.models.User
-import com.instructure.panda_annotations.*
import com.instructure.student.PendingSubmissionComment
import com.instructure.student.db.Db
import com.instructure.student.db.getInstance
@@ -131,7 +135,10 @@ class SubmissionCommentsRenderTest: StudentRenderTest() {
}
@Test
- @TestMetaData(Priority.COMMON,FeatureCategory.ASSIGNMENTS,TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
+ @TestMetaData(
+ Priority.COMMON,
+ FeatureCategory.ASSIGNMENTS,
+ TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
fun testSingleComment() {
val state = SubmissionCommentsViewState(
commentStates = listOf(commentItemIsAudience),
@@ -143,7 +150,10 @@ class SubmissionCommentsRenderTest: StudentRenderTest() {
}
@Test
- @TestMetaData(Priority.COMMON,FeatureCategory.ASSIGNMENTS,TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
+ @TestMetaData(
+ Priority.COMMON,
+ FeatureCategory.ASSIGNMENTS,
+ TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
fun testSingleSubmission() {
val state = SubmissionCommentsViewState(
commentStates = listOf(submissionItem)
@@ -153,7 +163,10 @@ class SubmissionCommentsRenderTest: StudentRenderTest() {
}
@Test
- @TestMetaData(Priority.COMMON,FeatureCategory.ASSIGNMENTS,TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
+ @TestMetaData(
+ Priority.COMMON,
+ FeatureCategory.ASSIGNMENTS,
+ TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
fun testSinglePendingComment() {
val state = SubmissionCommentsViewState(
commentStates = listOf(pendingCommentItem)
@@ -163,7 +176,10 @@ class SubmissionCommentsRenderTest: StudentRenderTest() {
}
@Test
- @TestMetaData(Priority.COMMON,FeatureCategory.ASSIGNMENTS,TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
+ @TestMetaData(
+ Priority.COMMON,
+ FeatureCategory.ASSIGNMENTS,
+ TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
fun testSingleCommentDisplaysAuthorPronoun() {
val commentItem = commentItemIsAudience.copy(authorPronouns = "Pro/Noun")
val state = SubmissionCommentsViewState(commentStates = listOf(commentItem))
@@ -172,7 +188,10 @@ class SubmissionCommentsRenderTest: StudentRenderTest() {
}
@Test
- @TestMetaData(Priority.COMMON,FeatureCategory.ASSIGNMENTS,TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
+ @TestMetaData(
+ Priority.COMMON,
+ FeatureCategory.ASSIGNMENTS,
+ TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
fun testSingleSubmissionDisplaysAuthorPronoun() {
val commentItem = submissionItem.copy(authorPronouns = "Pro/Noun")
val state = SubmissionCommentsViewState(commentStates = listOf(commentItem))
@@ -181,7 +200,10 @@ class SubmissionCommentsRenderTest: StudentRenderTest() {
}
@Test
- @TestMetaData(Priority.COMMON,FeatureCategory.ASSIGNMENTS,TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
+ @TestMetaData(
+ Priority.COMMON,
+ FeatureCategory.ASSIGNMENTS,
+ TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
fun testSinglePendingCommentDisplaysAuthorPronoun() {
val commentItem = pendingCommentItem.copy(authorPronouns = "Pro/Noun")
val state = SubmissionCommentsViewState(commentStates = listOf(commentItem))
@@ -190,7 +212,10 @@ class SubmissionCommentsRenderTest: StudentRenderTest() {
}
@Test
- @TestMetaData(Priority.COMMON,FeatureCategory.ASSIGNMENTS,TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
+ @TestMetaData(
+ Priority.COMMON,
+ FeatureCategory.ASSIGNMENTS,
+ TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
fun testEmptyState() {
val state = SubmissionCommentsViewState(
commentStates = listOf(CommentItemState.Empty)
@@ -200,7 +225,10 @@ class SubmissionCommentsRenderTest: StudentRenderTest() {
}
@Test
- @TestMetaData(Priority.COMMON,FeatureCategory.ASSIGNMENTS,TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
+ @TestMetaData(
+ Priority.COMMON,
+ FeatureCategory.ASSIGNMENTS,
+ TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
fun testFailedCommentDisplaysRetryAndDeleteOptions() {
db.setCommentError(true, pendingCommentItem.pendingComment.id)
val state = SubmissionCommentsViewState(
@@ -212,7 +240,10 @@ class SubmissionCommentsRenderTest: StudentRenderTest() {
}
@Test
- @TestMetaData(Priority.COMMON,FeatureCategory.ASSIGNMENTS,TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
+ @TestMetaData(
+ Priority.COMMON,
+ FeatureCategory.ASSIGNMENTS,
+ TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
fun testMixedCommentsAndSubmission() {
val state = SubmissionCommentsViewState(
commentStates = listOf(commentItemIsAudience, submissionItem, commentItemNotAudience)
@@ -228,7 +259,10 @@ class SubmissionCommentsRenderTest: StudentRenderTest() {
}
@Test
- @TestMetaData(Priority.COMMON,FeatureCategory.ASSIGNMENTS,TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
+ @TestMetaData(
+ Priority.COMMON,
+ FeatureCategory.ASSIGNMENTS,
+ TestCategory.RENDER,secondaryFeature = SecondaryFeatureCategory.ASSIGNMENT_COMMENTS)
fun testAudienceDistinction() {
val state = SubmissionCommentsViewState(
commentStates = listOf(commentItemIsAudience, commentItemNotAudience)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt
index 904e9a4b77..97b2ca7df8 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt
@@ -16,10 +16,10 @@
package com.instructure.student.ui.renderTests
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.student.FileSubmission
import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submission.file.UploadStatusSubmissionModel
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt
index 432f6a7c6c..ecb161ed32 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt
@@ -37,6 +37,7 @@ class GradeCellRenderTest : StudentRenderTest() {
val submittedTitle by OnViewWithId(R.id.submittedTitle)
val submittedSubtitle by OnViewWithId(R.id.submittedSubtitle)
val pointsLabel by OnViewWithId(R.id.pointsLabel)
+ val yourGrade by OnViewWithId(R.id.yourGrade)
val latePenalty by OnViewWithId(R.id.latePenalty)
val finalGrade by OnViewWithId(R.id.finalGrade)
val grade by OnViewWithId(R.id.grade)
@@ -122,7 +123,8 @@ class GradeCellRenderTest : StudentRenderTest() {
score = "91",
showPointsLabel = true,
outOf = "Out of 100 pts",
- latePenalty = "Late Penalty (-2 pts)",
+ yourGrade = "Your Grade: 91 pts",
+ latePenalty = "Late Penalty: -2 pts",
finalGrade = "Final Grade: 89 pts"
)
setupViewWithState(state)
@@ -132,6 +134,7 @@ class GradeCellRenderTest : StudentRenderTest() {
score.assertDisplayed()
pointsLabel.assertDisplayed()
outOf.assertDisplayed()
+ yourGrade.assertDisplayed()
latePenalty.assertDisplayed()
finalGrade.assertDisplayed()
}
@@ -140,6 +143,7 @@ class GradeCellRenderTest : StudentRenderTest() {
with(gradeCell) {
score.assertHasText(state.score)
outOf.assertHasText(state.outOf)
+ yourGrade.assertHasText(state.yourGrade)
latePenalty.assertHasText(state.latePenalty)
finalGrade.assertHasText(state.finalGrade)
}
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/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt
index 331d138f10..2bdebb30ac 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt
@@ -32,8 +32,10 @@ import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
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.espresso.swipeRight
import com.instructure.pandautils.utils.Const
@@ -42,6 +44,7 @@ import com.instructure.student.R
import com.instructure.student.activity.LoginActivity
import com.instructure.student.espresso.StudentHiltTestApplication_Application
import com.instructure.student.ui.pages.AboutPage
+import com.instructure.student.ui.pages.AllCoursesPage
import com.instructure.student.ui.pages.AnnotationCommentListPage
import com.instructure.student.ui.pages.AnnouncementListPage
import com.instructure.student.ui.pages.AssignmentDetailsPage
@@ -56,11 +59,11 @@ import com.instructure.student.ui.pages.CourseGradesPage
import com.instructure.student.ui.pages.DashboardPage
import com.instructure.student.ui.pages.DiscussionDetailsPage
import com.instructure.student.ui.pages.DiscussionListPage
-import com.instructure.student.ui.pages.EditDashboardPage
import com.instructure.student.ui.pages.ElementaryCoursePage
import com.instructure.student.ui.pages.ElementaryDashboardPage
import com.instructure.student.ui.pages.FileListPage
import com.instructure.student.ui.pages.FileUploadPage
+import com.instructure.student.ui.pages.GoToQuizPage
import com.instructure.student.ui.pages.GradesPage
import com.instructure.student.ui.pages.GroupBrowserPage
import com.instructure.student.ui.pages.HelpPage
@@ -77,6 +80,7 @@ import com.instructure.student.ui.pages.ModuleProgressionPage
import com.instructure.student.ui.pages.ModulesPage
import com.instructure.student.ui.pages.NewMessagePage
import com.instructure.student.ui.pages.NotificationPage
+import com.instructure.student.ui.pages.PageDetailsPage
import com.instructure.student.ui.pages.PageListPage
import com.instructure.student.ui.pages.PairObserverPage
import com.instructure.student.ui.pages.PandaAvatarPage
@@ -95,11 +99,11 @@ import com.instructure.student.ui.pages.ShareExtensionStatusPage
import com.instructure.student.ui.pages.ShareExtensionTargetPage
import com.instructure.student.ui.pages.SubmissionDetailsPage
import com.instructure.student.ui.pages.SyllabusPage
-import com.instructure.student.ui.pages.SyncSettingsPage
import com.instructure.student.ui.pages.TextSubmissionUploadPage
import com.instructure.student.ui.pages.TodoPage
import com.instructure.student.ui.pages.UrlSubmissionUploadPage
import com.instructure.student.ui.pages.offline.ManageOfflineContentPage
+import com.instructure.student.ui.pages.offline.OfflineSyncSettingsPage
import com.instructure.student.ui.pages.offline.SyncProgressPage
import dagger.hilt.android.testing.HiltAndroidRule
import instructure.rceditor.RCETextEditor
@@ -112,6 +116,8 @@ import javax.inject.Inject
abstract class StudentTest : CanvasTest() {
+ val device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
override val activityRule: InstructureActivityTestRule =
StudentActivityTestRule(LoginActivity::class.java)
@@ -147,7 +153,7 @@ abstract class StudentTest : CanvasTest() {
*/
val annotationCommentListPage = AnnotationCommentListPage()
val announcementListPage = AnnouncementListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn))
- val assignmentDetailsPage = AssignmentDetailsPage()
+ val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item))
val assignmentListPage = AssignmentListPage(Searchable(R.id.search, R.id.search_src_text))
val bookmarkPage = BookmarkPage()
val calendarEventPage = CalendarEventPage()
@@ -160,9 +166,9 @@ abstract class StudentTest : CanvasTest() {
val courseGradesPage = CourseGradesPage()
val dashboardPage = DashboardPage()
val leftSideNavigationDrawerPage = LeftSideNavigationDrawerPage()
- val discussionDetailsPage = DiscussionDetailsPage()
+ val discussionDetailsPage = DiscussionDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item))
val discussionListPage = DiscussionListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn))
- val editDashboardPage = EditDashboardPage()
+ val allCoursesPage = AllCoursesPage()
val fileListPage = FileListPage(Searchable(R.id.search, R.id.queryInput, R.id.clearButton, R.id.backButton))
val fileUploadPage = FileUploadPage()
val helpPage = HelpPage()
@@ -178,6 +184,7 @@ abstract class StudentTest : CanvasTest() {
val newMessagePage = NewMessagePage()
val notificationPage = NotificationPage()
val pageListPage = PageListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn))
+ val pageDetailsPage = PageDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item))
val pairObserverPage = PairObserverPage()
val pandaAvatarPage = PandaAvatarPage()
val peopleListPage = PeopleListPage()
@@ -187,6 +194,7 @@ abstract class StudentTest : CanvasTest() {
val qrLoginPage = QRLoginPage()
val quizListPage = QuizListPage()
val quizTakingPage = QuizTakingPage()
+ val goToQuizPage = GoToQuizPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item))
val remoteConfigSettingsPage = RemoteConfigSettingsPage()
val settingsPage = SettingsPage()
val submissionDetailsPage = SubmissionDetailsPage()
@@ -202,7 +210,7 @@ abstract class StudentTest : CanvasTest() {
val importantDatesPage = ImportantDatesPage()
val shareExtensionTargetPage = ShareExtensionTargetPage()
val shareExtensionStatusPage = ShareExtensionStatusPage()
- val syncSettingsPage = SyncSettingsPage()
+ val offlineSyncSettingsPage = OfflineSyncSettingsPage()
val manageOfflineContentPage = ManageOfflineContentPage()
val syncProgressPage = SyncProgressPage()
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 991c0e1778..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,7 +59,11 @@ fun StudentTest.slowLogIn(enrollmentType: String = EnrollmentTypes.STUDENT_ENROL
return user
}
-fun StudentTest.seedDataForK5(
+fun StudentTest.openOverflowMenu() {
+ Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext)
+}
+
+fun seedDataForK5(
teachers: Int = 0,
tas: Int = 0,
pastCourses: Int = 0,
@@ -69,7 +74,8 @@ fun StudentTest.seedDataForK5(
announcements: Int = 0,
discussions: Int = 0,
syllabusBody: String? = null,
- gradingPeriods: Boolean = false): SeedApi.SeededDataApiModel {
+ gradingPeriods: Boolean = false
+): SeedApi.SeededDataApiModel {
val request = SeedApi.SeedDataRequest (
teachers = teachers,
@@ -88,7 +94,7 @@ fun StudentTest.seedDataForK5(
return SeedApi.seedDataForSubAccount(request)
}
-fun StudentTest.seedData(
+fun seedData(
teachers: Int = 0,
tas: Int = 0,
pastCourses: Int = 0,
@@ -100,7 +106,8 @@ fun StudentTest.seedData(
locked: Boolean = false,
discussions: Int = 0,
syllabusBody: String? = null,
- gradingPeriods: Boolean = false): SeedApi.SeededDataApiModel {
+ gradingPeriods: Boolean = false
+): SeedApi.SeededDataApiModel {
val request = SeedApi.SeedDataRequest (
teachers = teachers,
@@ -119,15 +126,16 @@ fun StudentTest.seedData(
return SeedApi.seedData(request)
}
-fun StudentTest.seedAssignments(
- courseId: Long,
- assignments: Int = 1,
- withDescription: Boolean = false,
- lockAt: String = "",
- unlockAt: String = "",
- dueAt: String = "",
- submissionTypes: List = emptyList(),
- teacherToken: String): List {
+fun seedAssignments(
+ courseId: Long,
+ assignments: Int = 1,
+ withDescription: Boolean = false,
+ lockAt: String = "",
+ unlockAt: String = "",
+ dueAt: String = "",
+ submissionTypes: List = emptyList(),
+ teacherToken: String
+): List {
return AssignmentsApi.seedAssignments(AssignmentsApi.CreateAssignmentRequest(
courseId = courseId,
@@ -203,7 +211,7 @@ fun StudentTest.tokenLoginElementary(user: CanvasUserApiModel) {
elementaryDashboardPage.assertPageObjects()
}
-fun StudentTest.routeTo(route: String) {
+fun routeTo(route: String) {
val url = "canvas-student://${CanvasNetworkAdapter.canvasDomain}/$route"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
val context = InstrumentationRegistry.getInstrumentation().targetContext
@@ -213,7 +221,7 @@ fun StudentTest.routeTo(route: String) {
context.startActivity(intent)
}
-fun StudentTest.routeTo(route: String, domain: String) {
+fun routeTo(route: String, domain: String) {
val url = "canvas-student://$domain/$route"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
val context = InstrumentationRegistry.getInstrumentation().targetContext
@@ -223,39 +231,41 @@ fun StudentTest.routeTo(route: String, domain: String) {
context.startActivity(intent)
}
-fun StudentTest.routeTo(route: Route, activity: FragmentActivity) {
+fun routeTo(route: Route, activity: FragmentActivity) {
RouteMatcher.route(activity, route)
}
-fun StudentTest.seedAssignmentSubmission(
- submissionSeeds: List,
- assignmentId: Long,
- courseId: Long,
- studentToken: String,
- commentSeeds: List = kotlin.collections.emptyList()
+fun seedAssignmentSubmission(
+ submissionSeeds: List,
+ assignmentId: Long,
+ courseId: Long,
+ studentToken: String,
+ commentSeeds: List = emptyList()
): List {
// Upload one submission file for each submission seed
submissionSeeds.forEach {
it.attachmentsList.add(
when (it.submissionType) {
- SubmissionType.ONLINE_UPLOAD -> uploadTextFile(courseId, assignmentId, studentToken,
+ SubmissionType.ONLINE_UPLOAD -> uploadTextFile(
+ courseId, assignmentId, studentToken,
FileUploadType.ASSIGNMENT_SUBMISSION
)
else -> AttachmentApiModel(displayName="", fileName="", id=0L) // Not handled right now
}
- );
+ )
}
// Add attachments to comment seeds
commentSeeds.forEach {
- val fileAttachments: MutableList = kotlin.collections.mutableListOf()
+ val fileAttachments: MutableList = mutableListOf()
for (i in 0..it.amount) {
if (it.fileType != FileType.NONE) {
fileAttachments.add(when (it.fileType) {
- FileType.PDF -> kotlin.TODO()
- FileType.TEXT -> uploadTextFile(courseId, assignmentId, studentToken,
+ FileType.PDF -> TODO()
+ FileType.TEXT -> uploadTextFile(
+ courseId, assignmentId, studentToken,
FileUploadType.COMMENT_ATTACHMENT
)
else -> throw RuntimeException("Unknown file type passed into StudentTest.seedAssignmentSubmission") // Unknown type
@@ -266,19 +276,15 @@ fun StudentTest.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 StudentTest.uploadTextFile(courseId: Long, assignmentId: Long, token: String, fileUploadType: FileUploadType): AttachmentApiModel {
+fun uploadTextFile(
+ courseId: Long,
+ assignmentId: Long? = null,
+ token: String,
+ fileUploadType: FileUploadType
+): AttachmentApiModel {
// Create the file
val file = File(
@@ -294,10 +300,11 @@ fun StudentTest.uploadTextFile(courseId: Long, assignmentId: Long, token: String
// 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 @@
+
+
+
?)
@@ -130,6 +138,8 @@ abstract class CallbackActivity : ParentActivity(), OnUnreadCountInvalidated, No
getUnreadNotificationCount()
+ featureFlagProvider.fetchEnvironmentFeatureFlags()
+
initialCoreDataLoadingComplete()
} catch {
initialCoreDataLoadingComplete()
diff --git a/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt b/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt
index f73493b59f..75b0f78651 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt
@@ -27,11 +27,13 @@ import android.view.View
import android.view.Window
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
+import com.instructure.canvasapi2.managers.OAuthManager
import com.instructure.canvasapi2.models.AccountDomain
import com.instructure.canvasapi2.utils.Analytics
import com.instructure.canvasapi2.utils.AnalyticsEventConstants
import com.instructure.canvasapi2.utils.AnalyticsParamConstants
import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.canvasapi2.utils.weave.apiAsync
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryWeave
import com.instructure.loginapi.login.tasks.LogoutTask
@@ -45,6 +47,7 @@ import com.instructure.pandautils.utils.Utils.generateUserAgent
import com.instructure.student.R
import com.instructure.student.databinding.InterwebsToApplicationBinding
import com.instructure.student.databinding.LoadingCanvasViewBinding
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
import com.instructure.student.router.RouteMatcher
import com.instructure.student.tasks.StudentLogoutTask
import com.instructure.student.util.LoggingUtility
@@ -65,12 +68,15 @@ class InterwebsToApplication : AppCompatActivity() {
@Inject
lateinit var typefaceBehavior: TypefaceBehavior
+ @Inject
+ lateinit var alarmScheduler: AlarmScheduler
+
private var loadingJob: Job? = null
public override fun onCreate(savedInstanceState: Bundle?) {
requestWindowFeature(Window.FEATURE_NO_TITLE)
super.onCreate(savedInstanceState)
- setContentView(R.layout.interwebs_to_application)
+ setContentView(binding.root)
loadingBinding = LoadingCanvasViewBinding.bind(binding.root)
loadingBinding.loadingRoute.visibility = View.VISIBLE
@@ -106,7 +112,13 @@ class InterwebsToApplication : AppCompatActivity() {
// This is an App Link from a QR code, let's try to login the user and launch navigationActivity
try {
if(signedIn) { // If the user is already signed in, use the QR Switch
- StudentLogoutTask(type = LogoutTask.Type.QR_CODE_SWITCH, uri = data, canvasForElementaryFeatureFlag = featureFlagProvider.getCanvasForElementaryFlag(), typefaceBehavior = typefaceBehavior).execute()
+ StudentLogoutTask(
+ type = LogoutTask.Type.QR_CODE_SWITCH,
+ uri = data,
+ canvasForElementaryFeatureFlag = featureFlagProvider.getCanvasForElementaryFlag(),
+ typefaceBehavior = typefaceBehavior,
+ alarmScheduler = alarmScheduler
+ ).execute()
finish()
return@tryWeave
}
@@ -117,6 +129,13 @@ class InterwebsToApplication : AppCompatActivity() {
val tokenResponse = performSSOLogin(data, this@InterwebsToApplication)
+ val authResult = apiAsync { OAuthManager.getAuthenticatedSession(ApiPrefs.fullDomain, it) }.await()
+ if (authResult.isSuccess) {
+ authResult.dataOrNull?.sessionUrl?.let {
+ binding.dummyWebView.loadUrl(it)
+ }
+ }
+
val canvasForElementary = featureFlagProvider.getCanvasForElementaryFlag()
// Add delay for animation and launch Navigation Activity
diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt
index 674981de32..59da6b00bd 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt
@@ -52,9 +52,23 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.instructure.canvasapi2.CanvasRestAdapter
import com.instructure.canvasapi2.managers.GroupManager
import com.instructure.canvasapi2.managers.UserManager
-import com.instructure.canvasapi2.models.*
-import com.instructure.canvasapi2.utils.*
-import com.instructure.canvasapi2.utils.weave.*
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Group
+import com.instructure.canvasapi2.models.LaunchDefinition
+import com.instructure.canvasapi2.models.StorageQuotaExceededError
+import com.instructure.canvasapi2.models.User
+import com.instructure.canvasapi2.utils.APIHelper
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.canvasapi2.utils.LocaleUtils
+import com.instructure.canvasapi2.utils.Logger
+import com.instructure.canvasapi2.utils.MasqueradeHelper
+import com.instructure.canvasapi2.utils.Pronouns
+import com.instructure.canvasapi2.utils.weave.WeaveJob
+import com.instructure.canvasapi2.utils.weave.awaitApi
+import com.instructure.canvasapi2.utils.weave.catch
+import com.instructure.canvasapi2.utils.weave.tryLaunch
+import com.instructure.canvasapi2.utils.weave.tryWeave
+import com.instructure.canvasapi2.utils.weave.weave
import com.instructure.interactions.FragmentInteractions
import com.instructure.interactions.FullScreenInteractions
import com.instructure.interactions.Navigation
@@ -77,21 +91,54 @@ import com.instructure.pandautils.room.offline.DatabaseProvider
import com.instructure.pandautils.room.offline.OfflineDatabase
import com.instructure.pandautils.typeface.TypefaceBehavior
import com.instructure.pandautils.update.UpdateManager
-import com.instructure.pandautils.utils.*
+import com.instructure.pandautils.utils.ActivityResult
+import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.NetworkStateProvider
+import com.instructure.pandautils.utils.OnActivityResults
+import com.instructure.pandautils.utils.OnBackStackChangedEvent
+import com.instructure.pandautils.utils.PermissionReceiver
+import com.instructure.pandautils.utils.ProfileUtils
import com.instructure.pandautils.utils.RequestCodes.CAMERA_PIC_REQUEST
import com.instructure.pandautils.utils.RequestCodes.PICK_FILE_FROM_DEVICE
import com.instructure.pandautils.utils.RequestCodes.PICK_IMAGE_GALLERY
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.pandautils.utils.applyTheme
+import com.instructure.pandautils.utils.hideKeyboard
+import com.instructure.pandautils.utils.items
+import com.instructure.pandautils.utils.onClickWithRequireNetwork
+import com.instructure.pandautils.utils.post
+import com.instructure.pandautils.utils.postSticky
+import com.instructure.pandautils.utils.setGone
+import com.instructure.pandautils.utils.setVisible
+import com.instructure.pandautils.utils.setupAsBackButton
+import com.instructure.pandautils.utils.toast
import com.instructure.student.R
import com.instructure.student.databinding.ActivityNavigationBinding
import com.instructure.student.databinding.LoadingCanvasViewBinding
import com.instructure.student.databinding.NavigationDrawerBinding
import com.instructure.student.dialog.BookmarkCreationDialog
-import com.instructure.student.events.*
+import com.instructure.student.events.CoreDataFinishedLoading
+import com.instructure.student.events.CourseColorOverlayToggledEvent
+import com.instructure.student.events.ShowConfettiEvent
+import com.instructure.student.events.ShowGradesToggledEvent
+import com.instructure.student.events.StatusBarColorChangeEvent
+import com.instructure.student.events.UserUpdatedEvent
+import com.instructure.student.features.assignments.reminder.AlarmScheduler
import com.instructure.student.features.files.list.FileListFragment
import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment
import com.instructure.student.features.navigation.NavigationRepository
import com.instructure.student.flutterChannels.FlutterComm
-import com.instructure.student.fragment.*
+import com.instructure.student.fragment.BookmarksFragment
+import com.instructure.student.fragment.CalendarEventFragment
+import com.instructure.student.fragment.CalendarFragment
+import com.instructure.student.fragment.DashboardFragment
+import com.instructure.student.fragment.InboxComposeMessageFragment
+import com.instructure.student.fragment.InboxConversationFragment
+import com.instructure.student.fragment.InboxRecipientsFragment
+import com.instructure.student.fragment.LtiLaunchFragment
+import com.instructure.student.fragment.NotificationListFragment
+import com.instructure.student.fragment.ToDoListFragment
import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadEffectHandler
import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.ui.SubmissionDetailsEmptyContentFragment
import com.instructure.student.navigation.AccountMenuItem
@@ -105,11 +152,16 @@ import com.instructure.student.util.Analytics
import com.instructure.student.util.AppShortcutManager
import com.instructure.student.util.StudentPrefs
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
-import java.util.*
+import java.util.ArrayDeque
+import java.util.Deque
import javax.inject.Inject
private const val BOTTOM_NAV_SCREEN = "bottomNavScreen"
@@ -143,9 +195,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
@Inject
lateinit var databaseProvider: DatabaseProvider
- @Inject
- lateinit var featureFlagProvider: FeatureFlagProvider
-
@Inject
lateinit var repository: NavigationRepository
@@ -158,6 +207,9 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
@Inject
lateinit var firebaseCrashlytics: FirebaseCrashlytics
+ @Inject
+ lateinit var alarmScheduler: AlarmScheduler
+
private var routeJob: WeaveJob? = null
private var debounceJob: Job? = null
private var drawerItemSelectedJob: Job? = null
@@ -207,7 +259,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
StudentLogoutTask(
if (ApiPrefs.isStudentView) LogoutTask.Type.LOGOUT else LogoutTask.Type.SWITCH_USERS,
typefaceBehavior = typefaceBehavior,
- databaseProvider = databaseProvider
+ databaseProvider = databaseProvider,
+ alarmScheduler = alarmScheduler
).execute()
}
R.id.navigationDrawerItem_logout -> {
@@ -217,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)
@@ -230,7 +284,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
R.id.navigationDrawerItem_stopMasquerading -> {
MasqueradeHelper.stopMasquerading(startActivityClass)
}
- R.id.navigationDrawerSettings -> startActivity(Intent(applicationContext, SettingsActivity::class.java))
+ R.id.navigationDrawerSettings -> startActivity(SettingsActivity.createIntent(applicationContext, featureFlagProvider.offlineEnabled()))
}
}
}
@@ -305,8 +359,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
setupNavDrawerItems()
- loadFeatureFlags()
-
checkAppUpdates()
val savedBottomScreens = savedInstanceState?.getStringArrayList(BOTTOM_SCREENS_BUNDLE_KEY)
@@ -330,6 +382,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
} catch {
firebaseCrashlytics.recordException(it)
}
+
+ scheduleAlarms()
}
private fun handleTokenCheck(online: Boolean?) {
@@ -339,18 +393,17 @@ 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()
}
}
}
}
- private fun loadFeatureFlags() {
- lifecycleScope.launch {
- featureFlagProvider.fetchEnvironmentFeatureFlags()
- }
- }
-
private fun requestNotificationsPermission() {
if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
notificationsPermissionContract.launch(Manifest.permission.POST_NOTIFICATIONS)
@@ -398,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()
@@ -675,7 +728,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
override fun overrideFont() {
super.overrideFont()
- typefaceBehavior.overrideFont(navigationBehavior.fontFamily.fontPath)
+ typefaceBehavior.overrideFont(navigationBehavior.canvasFont)
}
//endregion
@@ -905,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()
@@ -1226,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/activity/SettingsActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt
index 78a7509f68..edaed771de 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt
@@ -18,9 +18,11 @@ package com.instructure.student.activity
import android.content.Context
import android.content.Intent
+import android.content.res.Configuration
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
+import com.instructure.interactions.FragmentInteractions
import com.instructure.pandautils.analytics.SCREEN_VIEW_SETTINGS
import com.instructure.pandautils.analytics.ScreenView
import com.instructure.pandautils.binding.viewBinding
@@ -31,6 +33,8 @@ import com.instructure.student.databinding.ActivitySettingsBinding
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
+private const val OFFLINE_ENABLED = "offlineEnabled"
+
@ScreenView(SCREEN_VIEW_SETTINGS)
@AndroidEntryPoint
class SettingsActivity : AppCompatActivity(){
@@ -40,10 +44,12 @@ class SettingsActivity : AppCompatActivity(){
private val binding by viewBinding(ActivitySettingsBinding::inflate)
+ var offlineEnabled: Boolean = false
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ offlineEnabled = intent.getBooleanExtra(OFFLINE_ENABLED, false)
setContentView(binding.root)
-
networkStateProvider.isOnlineLiveData.observe(this) { isOnline ->
binding.offlineIndicator.root.setVisible(!isOnline)
}
@@ -51,6 +57,17 @@ class SettingsActivity : AppCompatActivity(){
private val currentFragment: Fragment? get() = supportFragmentManager.fragments.last()
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ applyThemeForAllFragments()
+ }
+
+ private fun applyThemeForAllFragments() {
+ supportFragmentManager.fragments.forEach {
+ (it as? FragmentInteractions)?.applyTheme()
+ }
+ }
+
fun addFragment(fragment: Fragment) {
val ft = supportFragmentManager.beginTransaction()
currentFragment?.let { ft.hide(it) }
@@ -60,8 +77,10 @@ class SettingsActivity : AppCompatActivity(){
}
companion object {
- fun createIntent(context: Context): Intent {
- return Intent(context, SettingsActivity::class.java)
+ fun createIntent(context: Context, offlineEnabled: Boolean): Intent {
+ return Intent(context, SettingsActivity::class.java).apply {
+ putExtra(OFFLINE_ENABLED, offlineEnabled)
+ }
}
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/BaseListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/BaseListRecyclerAdapter.kt
index d801682e61..773e3221f7 100644
--- a/apps/student/src/main/java/com/instructure/student/adapter/BaseListRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/adapter/BaseListRecyclerAdapter.kt
@@ -102,8 +102,8 @@ abstract class BaseListRecyclerAdapter, T : Recycler
fun onCallbackFinished() {
isLoadedFirstPage = true
shouldShowLoadingFooter()
- adapterToRecyclerViewCallback.setDisplayNoConnection(false)
- adapterToRecyclerViewCallback.setIsEmpty(isAllPagesLoaded && size() == 0)
+ adapterToRecyclerViewCallback?.setDisplayNoConnection(false)
+ adapterToRecyclerViewCallback?.setIsEmpty(isAllPagesLoaded && size() == 0)
}
/**
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/BookmarkRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/BookmarkRecyclerAdapter.kt
index e65d179e7c..e3f695019e 100644
--- a/apps/student/src/main/java/com/instructure/student/adapter/BookmarkRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/adapter/BookmarkRecyclerAdapter.kt
@@ -18,7 +18,6 @@
package com.instructure.student.adapter
import android.content.Context
-import android.os.Handler
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
@@ -30,7 +29,6 @@ import com.instructure.canvasapi2.models.Bookmark
import com.instructure.canvasapi2.utils.APIHelper
import com.instructure.canvasapi2.utils.ApiType
import com.instructure.canvasapi2.utils.LinkHeaders
-import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.ColorUtils
import com.instructure.pandautils.utils.textAndIconColor
import com.instructure.student.R
@@ -39,7 +37,7 @@ import com.instructure.student.router.RouteMatcher
import com.instructure.student.util.CacheControlFlags
import retrofit2.Call
import retrofit2.Response
-import java.util.Locale
+import java.util.*
class BookmarkRecyclerAdapter(context: Context, isShortcutActivity: Boolean, private val mAdapterToFragmentCallback: BookmarkAdapterToFragmentCallback)
: BaseListRecyclerAdapter(context, Bookmark::class.java) {
@@ -81,7 +79,7 @@ class BookmarkRecyclerAdapter(context: Context, isShortcutActivity: Boolean, pri
override fun onFail(call: Call>?, error: Throwable, response: Response<*>?) {
if (response != null && !APIHelper.isCachedResponse(response) || !APIHelper.hasNetworkConnection()) {
- adapterToRecyclerViewCallback.setIsEmpty(true)
+ adapterToRecyclerViewCallback?.setIsEmpty(true)
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt
index 725f9456d3..66b1837c5f 100644
--- a/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt
@@ -135,6 +135,8 @@ class DashboardRecyclerAdapter(
val dashboardCards = repository.getDashboardCourses(isRefresh)
val syncedCourseIds = repository.getSyncedCourseIds()
+ resetData()
+
mCourseMap = courses.associateBy { it.id }
// Map not null is needed because the dashboard api can return unpublished courses
@@ -158,10 +160,10 @@ class DashboardRecyclerAdapter(
notifyDataSetChanged()
isAllPagesLoaded = true
- if (itemCount == 0) adapterToRecyclerViewCallback.setIsEmpty(true)
+ if (itemCount == 0) adapterToRecyclerViewCallback?.setIsEmpty(true)
mAdapterToFragmentCallback.onRefreshFinished()
} catch {
- adapterToRecyclerViewCallback.setDisplayNoConnection(true)
+ adapterToRecyclerViewCallback?.setDisplayNoConnection(true)
mAdapterToFragmentCallback.onRefreshFinished()
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/ExpandableRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/ExpandableRecyclerAdapter.kt
index 9f99e78f10..d26c813838 100644
--- a/apps/student/src/main/java/com/instructure/student/adapter/ExpandableRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/adapter/ExpandableRecyclerAdapter.kt
@@ -35,14 +35,14 @@ abstract class ExpandableRecyclerAdapter() {
override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) {
removeItem(todo)
- adapterToRecyclerViewCallback.setIsEmpty(size() == 0)
+ adapterToRecyclerViewCallback?.setIsEmpty(size() == 0)
}
override fun onFail(call: Call?, error: Throwable, response: Response<*>?) {
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/ApplicationModule.kt b/apps/student/src/main/java/com/instructure/student/di/ApplicationModule.kt
new file mode 100644
index 0000000000..38d3f681b2
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/di/ApplicationModule.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ * 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 com.instructure.pandautils.utils.LogoutHelper
+import com.instructure.student.util.StudentLogoutHelper
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+class ApplicationModule {
+
+ @Provides
+ fun provideLogoutHelper(): LogoutHelper {
+ return StudentLogoutHelper()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/di/DashboardModule.kt b/apps/student/src/main/java/com/instructure/student/di/DashboardModule.kt
index e31afb88a7..ce8ef99516 100644
--- a/apps/student/src/main/java/com/instructure/student/di/DashboardModule.kt
+++ b/apps/student/src/main/java/com/instructure/student/di/DashboardModule.kt
@@ -19,6 +19,7 @@ package com.instructure.student.di
import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.apis.CourseAPI
import com.instructure.canvasapi2.apis.GroupAPI
+import com.instructure.canvasapi2.apis.UserAPI
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.pandautils.features.dashboard.edit.EditDashboardRouter
import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter
@@ -56,9 +57,10 @@ class DashboardModule {
fun provideDashboardNetworkDataSource(
courseApi: CourseAPI.CoursesInterface,
groupApi: GroupAPI.GroupInterface,
- apiPrefs: ApiPrefs
+ apiPrefs: ApiPrefs,
+ userApi: UserAPI.UsersInterface
): DashboardNetworkDataSource {
- return DashboardNetworkDataSource(courseApi, groupApi, apiPrefs)
+ return DashboardNetworkDataSource(courseApi, groupApi, apiPrefs, userApi)
}
@Provides
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/dialog/EditTextDialog.kt b/apps/student/src/main/java/com/instructure/student/dialog/EditTextDialog.kt
index be1de289a2..1cf2d568e4 100644
--- a/apps/student/src/main/java/com/instructure/student/dialog/EditTextDialog.kt
+++ b/apps/student/src/main/java/com/instructure/student/dialog/EditTextDialog.kt
@@ -66,7 +66,14 @@ class EditTextDialog : AppCompatDialogFragment() {
ViewStyler.themeEditText(requireContext(), binding.textInput, ThemePrefs.brandColor)
binding.textInput.setText(mDefaultText)
- binding.textInput.selectAll()
+
+ val endIndex = mDefaultText.lastIndexOf(".")
+ if (endIndex != -1) {
+ binding.textInput.setSelection(0, endIndex)
+ } else {
+ binding.textInput.selectAll()
+ }
+ binding.textInput.requestFocus()
val dialog = AlertDialog.Builder(requireContext())
.setCancelable(true)
diff --git a/apps/student/src/main/java/com/instructure/student/dialog/LegalDialogStyled.kt b/apps/student/src/main/java/com/instructure/student/dialog/LegalDialogStyled.kt
index d163ee5069..d0e1a5018c 100644
--- a/apps/student/src/main/java/com/instructure/student/dialog/LegalDialogStyled.kt
+++ b/apps/student/src/main/java/com/instructure/student/dialog/LegalDialogStyled.kt
@@ -87,7 +87,7 @@ class LegalDialogStyled : AppCompatDialogFragment() {
}
binding.privacyPolicy.onClick {
- val intent = InternalWebViewActivity.createIntent(activity, "https://www.instructure.com/canvas/privacy", getString(R.string.privacyPolicy), false)
+ val intent = InternalWebViewActivity.createIntent(activity, "https://www.instructure.com/policies/product-privacy-policy", getString(R.string.privacyPolicy), false)
requireContext().startActivity(intent)
dialog?.dismiss()
}
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 345659d12b..603dc0c104 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
@@ -20,11 +20,24 @@ package com.instructure.student.features.assignments.details
import android.app.Application
import android.content.Context
import android.content.res.Resources
-import androidx.lifecycle.*
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import com.instructure.canvasapi2.managers.SubmissionManager
import com.instructure.canvasapi2.models.*
import com.instructure.canvasapi2.models.Assignment.SubmissionType
-import com.instructure.canvasapi2.utils.*
+import com.instructure.canvasapi2.utils.Analytics
+import com.instructure.canvasapi2.utils.AnalyticsEventConstants
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.canvasapi2.utils.DateHelper
+import com.instructure.canvasapi2.utils.NumberHelper
+import com.instructure.canvasapi2.utils.Pronouns
+import com.instructure.canvasapi2.utils.isNullOrEmpty
+import com.instructure.canvasapi2.utils.isRtl
+import com.instructure.canvasapi2.utils.isValid
import com.instructure.interactions.bookmarks.Bookmarker
import com.instructure.interactions.router.RouterParams
import com.instructure.pandautils.BR
@@ -32,10 +45,17 @@ 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.utils.*
+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
+import com.instructure.pandautils.utils.HtmlContentFormatter
+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
@@ -44,7 +64,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.io.File
import java.text.DateFormat
-import java.util.*
+import java.util.Date
+import java.util.Locale
import javax.inject.Inject
import com.instructure.student.Submission as DatabaseSubmission
@@ -56,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 {
@@ -95,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)
@@ -102,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()
@@ -227,7 +267,6 @@ class AssignmentDetailsViewModel @Inject constructor(
}
}
- @Suppress("DEPRECATION")
private suspend fun getViewData(assignment: Assignment, hasDraft: Boolean): AssignmentDetailsViewData {
val points = if (restrictQuantitativeData) {
""
@@ -242,7 +281,7 @@ class AssignmentDetailsViewModel @Inject constructor(
val assignmentState = AssignmentUtils2.getAssignmentState(assignment, assignment.submission, false)
// Don't mark LTI assignments as missing when overdue as they usually won't have a real submission for it
- val isMissing = assignment.submission?.missing.orDefault() || (assignment.turnInType != Assignment.TurnInType.EXTERNAL_TOOL
+ val isMissing = assignment.isMissing() || (assignment.turnInType != Assignment.TurnInType.EXTERNAL_TOOL
&& assignment.dueAt != null
&& assignmentState == AssignmentUtils2.ASSIGNMENT_STATE_MISSING)
@@ -269,8 +308,11 @@ class AssignmentDetailsViewModel @Inject constructor(
val submittedStatusIcon = if (assignment.isSubmitted) R.drawable.ic_complete_solid else R.drawable.ic_no
// Submission Status under title - We only show Graded or nothing at all for PAPER/NONE
- val submissionStatusVisible = assignmentState == AssignmentUtils2.ASSIGNMENT_STATE_GRADED
- || (assignment.turnInType != Assignment.TurnInType.ON_PAPER && assignment.turnInType != Assignment.TurnInType.NONE)
+ val submissionStatusVisible =
+ assignmentState == AssignmentUtils2.ASSIGNMENT_STATE_GRADED
+ || assignmentState == AssignmentUtils2.ASSIGNMENT_STATE_MISSING
+ || assignmentState == AssignmentUtils2.ASSIGNMENT_STATE_GRADED_MISSING
+ || (assignment.turnInType != Assignment.TurnInType.ON_PAPER && assignment.turnInType != Assignment.TurnInType.NONE)
if (assignment.isLocked) {
val lockedMessage = if (assignment.lockInfo?.contextModule != null) {
@@ -340,10 +382,11 @@ class AssignmentDetailsViewModel @Inject constructor(
)
// Observers shouldn't see the submit button OR if the course is soft concluded
- val submitVisible = if (isObserver || !course?.isBetweenValidDateRange().orDefault()) {
- false
- } else {
- when (assignment.turnInType) {
+ val submitVisible = when {
+ isObserver -> false
+ !course?.isBetweenValidDateRange().orDefault() -> false
+ assignment.submission?.excused.orDefault() -> false
+ else -> when (assignment.turnInType) {
Assignment.TurnInType.QUIZ, Assignment.TurnInType.DISCUSSION -> true
Assignment.TurnInType.ONLINE, Assignment.TurnInType.EXTERNAL_TOOL -> assignment.isAllowedToSubmit
else -> false
@@ -443,7 +486,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())
)
}
@@ -451,6 +496,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)
@@ -556,4 +616,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/gradecellview/GradeCellViewData.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/gradecellview/GradeCellViewData.kt
index 7fb3b6eae8..fe8c7ca580 100644
--- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/gradecellview/GradeCellViewData.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/gradecellview/GradeCellViewData.kt
@@ -24,6 +24,7 @@ data class GradeCellViewData(
val grade: String = "",
val gradeCellContentDescription: String = "",
val outOf: String = "",
+ val yourGrade: String = "",
val latePenalty: String = "",
val finalGrade: String = "",
val stats: GradeCellViewState.GradeStats? = null
@@ -39,7 +40,6 @@ data class GradeCellViewData(
}
companion object {
- @Suppress("DEPRECATION")
fun fromSubmission(
resources: Resources,
courseColor: ThemedColor,
@@ -145,7 +145,7 @@ data class GradeCellViewData(
gradeCellContentDescription = contentDescription,
)
} else {
- val score = NumberHelper.formatDecimal(submission.enteredScore, 2, true)
+ val score = NumberHelper.formatDecimal(submission.score, 2, true)
val chartPercent = (submission.enteredScore / assignment.pointsPossible).coerceIn(0.0, 1.0).toFloat()
// If grading type is Points, don't show the grade since we're already showing it as the score
var grade = if (assignment.gradingType != Assignment.POINTS_TYPE) submission.grade.orEmpty() else ""
@@ -170,12 +170,15 @@ data class GradeCellViewData(
var latePenalty = ""
var finalGrade = ""
+ var yourGrade = ""
// Adjust for late penalty, if any
if (submission.pointsDeducted.orDefault() > 0.0) {
grade = "" // Grade will be shown in the 'final grade' text
val pointsDeducted = NumberHelper.formatDecimal(submission.pointsDeducted.orDefault(), 2, true)
- latePenalty = resources.getString(R.string.latePenalty, pointsDeducted)
+ val achievedScore = NumberHelper.formatDecimal(submission.enteredScore, 2, true)
+ yourGrade = resources.getString(R.string.yourGrade, achievedScore)
+ latePenalty = resources.getString(R.string.latePenaltyUpdated, pointsDeducted)
finalGrade = resources.getString(R.string.finalGradeFormatted, submission.grade)
}
@@ -210,6 +213,7 @@ data class GradeCellViewData(
grade = grade,
gradeCellContentDescription = gradeCellContentDescription,
outOf = outOfText,
+ yourGrade = yourGrade,
latePenalty = latePenalty,
finalGrade = finalGrade,
stats = stats
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/list/AssignmentListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt
index e198678fb9..97c13658cc 100644
--- a/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt
@@ -75,7 +75,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT)
- private lateinit var recyclerAdapter: AssignmentListRecyclerAdapter
+ private var recyclerAdapter: AssignmentListRecyclerAdapter? = null
private var termAdapter: TermSpinnerAdapter? = null
private var filterPosition = 0
@@ -118,7 +118,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
override fun onRefreshFinished() {
if (!isAdded) return // Refresh can finish after user has left screen, causing emptyView to be null
setRefreshing(false)
- if (recyclerAdapter.size() == 0) {
+ if (recyclerAdapter?.size() == 0) {
setEmptyView(binding.emptyView, R.drawable.ic_panda_space, R.string.noAssignments, R.string.noAssignmentsSubtext)
}
}
@@ -140,14 +140,16 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
binding.sortByTextView.setText(sortOrder.buttonTextRes)
binding.sortByButton.contentDescription = getString(sortOrder.contentDescriptionRes)
- configureRecyclerView(
+ recyclerAdapter?.let {
+ configureRecyclerView(
view,
requireContext(),
- recyclerAdapter,
+ it,
R.id.swipeRefreshLayout,
R.id.emptyView,
R.id.listView
- )
+ )
+ }
binding.appbar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, i ->
// Workaround for Toolbar not showing with swipe to refresh
@@ -230,7 +232,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
dialog.dismiss()
filterPosition = index
filter = AssignmentListFilter.values()[index]
- recyclerAdapter.filter = filter
+ recyclerAdapter?.filter = filter
updateBadge()
}
@@ -262,7 +264,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
} else {
emptyView.emptyViewText(getString(R.string.noItemsMatchingQuery, query))
}
- recyclerAdapter.searchQuery = query
+ recyclerAdapter?.searchQuery = query
}
ViewStyler.themeToolbarColored(requireActivity(), toolbar, canvasContext)
}
@@ -290,22 +292,22 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
termSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) {
if (adapter.getItem(i)!!.title == getString(R.string.assignmentsListAllGradingPeriods)) {
- recyclerAdapter.loadAssignment()
+ recyclerAdapter?.loadAssignment()
} else {
- recyclerAdapter.loadAssignmentsForGradingPeriod(adapter.getItem(i)!!.id, true)
+ recyclerAdapter?.loadAssignmentsForGradingPeriod(adapter.getItem(i)!!.id, true)
termSpinner.isEnabled = false
adapter.isLoading = true
adapter.notifyDataSetChanged()
}
- recyclerAdapter.currentGradingPeriod = adapter.getItem(i)
+ recyclerAdapter?.currentGradingPeriod = adapter.getItem(i)
}
override fun onNothingSelected(adapterView: AdapterView<*>) {}
}
// If we have a "current" grading period select it
- if (hasGradingPeriods && recyclerAdapter.currentGradingPeriod != null) {
- val position = adapter.getPositionForId(recyclerAdapter.currentGradingPeriod?.id ?: 0)
+ if (hasGradingPeriods && recyclerAdapter?.currentGradingPeriod != null) {
+ val position = adapter.getPositionForId(recyclerAdapter?.currentGradingPeriod?.id ?: 0)
if (position != -1) {
termSpinner.setSelection(position)
} else {
@@ -318,16 +320,17 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
override fun onConfigurationChanged(newConfig: Configuration) = with(binding) {
super.onConfigurationChanged(newConfig)
- configureRecyclerView(
+ recyclerAdapter?.let {
+ configureRecyclerView(
requireView(),
requireContext(),
- recyclerAdapter,
+ it,
R.id.swipeRefreshLayout,
R.id.emptyView,
- R.id.listView,
- R.string.noAssignments
- )
- if (recyclerAdapter.size() == 0) {
+ R.id.listView
+ )
+ }
+ if (recyclerAdapter?.size() == 0) {
emptyView.changeTextSize()
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
if (isTablet) {
@@ -348,7 +351,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
override fun onDestroy() {
super.onDestroy()
- recyclerAdapter.cancel()
+ recyclerAdapter?.cancel()
}
companion object {
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/dashboard/DashboardLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardLocalDataSource.kt
index d6f4fd1f7d..ff40b2e37b 100644
--- a/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardLocalDataSource.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardLocalDataSource.kt
@@ -16,8 +16,10 @@
*/
package com.instructure.student.features.dashboard
+import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.DashboardCard
+import com.instructure.canvasapi2.models.DashboardPositions
import com.instructure.canvasapi2.models.Group
import com.instructure.pandautils.room.offline.daos.DashboardCardDao
import com.instructure.pandautils.room.offline.entities.DashboardCardEntity
@@ -43,4 +45,28 @@ class DashboardLocalDataSource(
suspend fun saveDashboardCards(dashboardCards: List) {
dashboardCardDao.updateEntities(dashboardCards.map { DashboardCardEntity(it) })
}
+
+ suspend fun updateDashboardCardsOrder(dashboardPositions: DashboardPositions) {
+ val cards = dashboardCardDao.findAll()
+ val coursesWithPosition = dashboardPositions.positions
+ .map { Pair(CanvasContext.fromContextCode(it.key), it.value) }
+ .filter { it.first is Course }
+ .associate { Pair((it.first as Course).id, it.second) }
+
+ // If somehow we end up with different items in the positions response than the stored dashboard cards we should return and not update the positions
+ val cardIds = cards.map { it.id }.toSet()
+ val positionUpdateIds = coursesWithPosition.keys
+ if (cardIds != positionUpdateIds) return
+
+ val newCards = cards.map {
+ val newPosition = coursesWithPosition[it.id]
+ if (newPosition != null) {
+ it.copy(position = newPosition)
+ } else {
+ it
+ }
+ }
+
+ dashboardCardDao.updateEntities(newCards)
+ }
}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardNetworkDataSource.kt
index b60a138f73..d12d9f323e 100644
--- a/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardNetworkDataSource.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardNetworkDataSource.kt
@@ -18,17 +18,21 @@ package com.instructure.student.features.dashboard
import com.instructure.canvasapi2.apis.CourseAPI
import com.instructure.canvasapi2.apis.GroupAPI
+import com.instructure.canvasapi2.apis.UserAPI
import com.instructure.canvasapi2.builders.RestParams
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.DashboardCard
+import com.instructure.canvasapi2.models.DashboardPositions
import com.instructure.canvasapi2.models.Group
import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.canvasapi2.utils.DataResult
import com.instructure.canvasapi2.utils.depaginate
class DashboardNetworkDataSource(
private val courseApi: CourseAPI.CoursesInterface,
private val groupApi: GroupAPI.GroupInterface,
- private val apiPrefs: ApiPrefs
+ private val apiPrefs: ApiPrefs,
+ private val userApi: UserAPI.UsersInterface
): DashboardDataSource {
override suspend fun getCourses(forceNetwork: Boolean): List {
@@ -53,4 +57,8 @@ class DashboardNetworkDataSource(
override suspend fun getDashboardCards(forceNetwork: Boolean): List {
return courseApi.getDashboardCourses(RestParams(isForceReadFromNetwork = forceNetwork)).dataOrNull.orEmpty()
}
+
+ suspend fun updateDashboardPositions(dashboardPositions: DashboardPositions): DataResult {
+ return userApi.updateDashboardPositions(dashboardPositions, RestParams(isForceReadFromNetwork = true))
+ }
}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardRepository.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardRepository.kt
index 4d85ff6b22..29b546eb83 100644
--- a/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardRepository.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardRepository.kt
@@ -16,11 +16,11 @@
*/
package com.instructure.student.features.dashboard
-import com.instructure.canvasapi2.apis.CourseAPI
-import com.instructure.canvasapi2.builders.RestParams
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.DashboardCard
+import com.instructure.canvasapi2.models.DashboardPositions
import com.instructure.canvasapi2.models.Group
+import com.instructure.canvasapi2.utils.DataResult
import com.instructure.pandautils.repository.Repository
import com.instructure.pandautils.room.offline.daos.CourseDao
import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao
@@ -29,7 +29,7 @@ import com.instructure.pandautils.utils.NetworkStateProvider
class DashboardRepository(
private val localDataSource: DashboardLocalDataSource,
- networkDataSource: DashboardNetworkDataSource,
+ private val networkDataSource: DashboardNetworkDataSource,
networkStateProvider: NetworkStateProvider,
featureFlagProvider: FeatureFlagProvider,
private val courseSyncSettingsDao: CourseSyncSettingsDao,
@@ -45,11 +45,14 @@ class DashboardRepository(
}
suspend fun getDashboardCourses(forceNetwork: Boolean): List {
- val dashboardCards = dataSource().getDashboardCards(forceNetwork).sortedBy { it.position }
+ var dashboardCards = dataSource().getDashboardCards(forceNetwork)
+ if (dashboardCards.all { it.position == Int.MAX_VALUE }) {
+ dashboardCards = dashboardCards.mapIndexed { index, dashboardCard -> dashboardCard.copy(position = index) }
+ }
if (isOnline() && isOfflineEnabled()) {
localDataSource.saveDashboardCards(dashboardCards)
}
- return dashboardCards
+ return dashboardCards.sortedBy { it.position }
}
suspend fun getSyncedCourseIds(): Set {
@@ -64,4 +67,12 @@ class DashboardRepository(
val syncedCourses = courseDao.findByIds(syncedCourseIds)
return syncedCourses.map { it.id }.toSet()
}
+
+ suspend fun updateDashboardPositions(dashboardPositions: DashboardPositions): DataResult {
+ val result = networkDataSource.updateDashboardPositions(dashboardPositions)
+ if (result is DataResult.Success) {
+ localDataSource.updateDashboardCardsOrder(dashboardPositions)
+ }
+ return networkDataSource.updateDashboardPositions(dashboardPositions)
+ }
}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt
index e76d4dbabe..bcc9257472 100644
--- a/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt
@@ -194,33 +194,29 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable {
//region Discussion Actions
private fun viewAttachments(remoteFiles: List) {
- if (repository.isOnline()) {
- // Only one file can be attached to a discussion
- val remoteFile = remoteFiles.firstOrNull() ?: return
-
- // Show lock message if file is locked
- if (remoteFile.lockedForUser) {
- if (remoteFile.lockExplanation.isValid()) {
- Snackbar.make(
- requireView(),
- remoteFile.lockExplanation!!,
- Snackbar.LENGTH_SHORT
- ).show()
- } else {
- Snackbar.make(
- requireView(),
- R.string.fileCurrentlyLocked,
- Snackbar.LENGTH_SHORT
- ).show()
- }
+ // Only one file can be attached to a discussion
+ val remoteFile = remoteFiles.firstOrNull() ?: return
+
+ // Show lock message if file is locked
+ if (remoteFile.lockedForUser) {
+ if (remoteFile.lockExplanation.isValid()) {
+ Snackbar.make(
+ requireView(),
+ remoteFile.lockExplanation!!,
+ Snackbar.LENGTH_SHORT
+ ).show()
+ } else {
+ Snackbar.make(
+ requireView(),
+ R.string.fileCurrentlyLocked,
+ Snackbar.LENGTH_SHORT
+ ).show()
}
-
- // Show attachment
- val attachment = remoteFile.mapToAttachment()
- openMedia(attachment.contentType, attachment.url, attachment.filename, canvasContext)
- } else {
- NoInternetConnectionDialog.show(requireFragmentManager())
}
+
+ // Show attachment
+ val attachment = remoteFile.mapToAttachment()
+ openMedia(attachment.contentType, attachment.url, attachment.filename, canvasContext, localFile = attachment.isLocalFile)
}
private fun showReplyView(discussionEntryId: Long) {
@@ -612,7 +608,8 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable {
canvasContext,
discussionTopicHeader,
discussionTopic!!.views,
- discussionEntryId
+ discussionEntryId,
+ repository.isOnline()
)
loadDiscussionTopicViews(html)
diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListRecyclerAdapter.kt
index b98cb662c6..62ffa37c47 100644
--- a/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListRecyclerAdapter.kt
@@ -35,7 +35,7 @@ import com.instructure.student.holders.NoViewholder
import com.instructure.student.interfaces.AdapterToFragmentCallback
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import java.util.Date
+import java.util.*
open class DiscussionListRecyclerAdapter(
context: Context,
@@ -125,7 +125,7 @@ open class DiscussionListRecyclerAdapter(
}
callback.onRefreshFinished()
onCallbackFinished(ApiType.API)
- adapterToRecyclerViewCallback.setIsEmpty(size() == 0)
+ adapterToRecyclerViewCallback?.setIsEmpty(size() == 0)
}
private fun getHeaderType(discussionTopicHeader: DiscussionTopicHeader): String {
diff --git a/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt
index 4c4bfbbfe4..2a36489673 100644
--- a/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt
@@ -96,6 +96,11 @@ class FileDetailsFragment : ParentFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
= inflater.inflate(R.layout.fragment_file_details, container, false)
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.downloadButton.setVisible(repository.isOnline())
+ }
+
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
getFileFolder()
@@ -131,8 +136,6 @@ class FileDetailsFragment : ParentFragment() {
requestPermissions(PermissionUtils.makeArray(PermissionUtils.WRITE_EXTERNAL_STORAGE), PermissionUtils.WRITE_FILE_PERMISSION_REQUEST_CODE)
}
}
-
- binding.downloadButton.setVisible(repository.isOnline())
}
override fun onMediaLoadingStarted() {
diff --git a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt
index 553bb47c90..9aa6614bed 100644
--- a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt
@@ -318,7 +318,7 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent
val isUserFiles = canvasContext.type == CanvasContext.Type.USER
if (recyclerAdapter == null) {
- recyclerAdapter = FileListRecyclerAdapter(requireContext(), canvasContext, getFileMenuOptions(folder, canvasContext, fileListRepository.isOnline()), folder, adapterCallback, fileListRepository)
+ recyclerAdapter = FileListRecyclerAdapter(requireContext(), canvasContext, getFileMenuOptions(folder, canvasContext, fileListRepository.isOnline(), folder), folder, adapterCallback, fileListRepository)
}
configureRecyclerView(requireView(), requireContext(), recyclerAdapter!!, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView)
@@ -362,7 +362,7 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent
val popup = PopupMenu(requireContext(), anchorView)
popup.inflate(R.menu.file_folder_options)
with(popup.menu) {
- val options = getFileMenuOptions(item, canvasContext, fileListRepository.isOnline())
+ val options = getFileMenuOptions(item, canvasContext, fileListRepository.isOnline(), folder)
// Only show alternate-open option for PDF files
findItem(R.id.openAlternate).isVisible = options.contains(FileMenuType.OPEN_IN_ALTERNATE)
findItem(R.id.download).isVisible = options.contains(FileMenuType.DOWNLOAD)
@@ -609,7 +609,7 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent
/**
* @return A list of possible actions the user is able to perform on the file/folder
*/
- fun getFileMenuOptions(fileFolder: FileFolder?, canvasContext: CanvasContext, isOnline: Boolean): List {
+ fun getFileMenuOptions(fileFolder: FileFolder?, canvasContext: CanvasContext, isOnline: Boolean, folder: FileFolder?): List {
if (fileFolder == null) return emptyList()
val options: MutableList = mutableListOf()
@@ -617,7 +617,8 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent
// We're in the user's files, they should have options in the options menu
if (!fileFolder.isLockedForUser) {
// File is not locked for this user
- if (!fileFolder.forSubmissions) {
+ val forSubmission = if (!fileFolder.isFile) fileFolder.forSubmissions else folder?.forSubmissions ?: false
+ if (!forSubmission) {
// File/folder is not for a submission, so we can rename/delete
with(options) {
add(FileMenuType.RENAME)
diff --git a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListRecyclerAdapter.kt
index 35f5dae3fd..29e867d69d 100644
--- a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListRecyclerAdapter.kt
@@ -68,7 +68,7 @@ open class FileListRecyclerAdapter(
}
override fun bindHolder(item: FileFolder, holder: FileViewHolder, position: Int) {
- holder.bind(item, contextColor, context, FileListFragment.getFileMenuOptions(item, canvasContext, fileListRepository.isOnline()), fileFolderCallback)
+ holder.bind(item, contextColor, context, FileListFragment.getFileMenuOptions(item, canvasContext, fileListRepository.isOnline(), folder), fileFolderCallback)
}
override fun createViewHolder(v: View, viewType: Int) = FileViewHolder(v)
diff --git a/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt
index d5b10ebda6..eb8dfb874d 100644
--- a/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt
@@ -74,7 +74,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
private var gradingScheme = emptyList()
private lateinit var allTermsGradingPeriod: GradingPeriod
- private lateinit var recyclerAdapter: GradesListRecyclerAdapter
+ private var recyclerAdapter: GradesListRecyclerAdapter? = null
private val course: Course
get() = canvasContext as Course
@@ -106,9 +106,9 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
//check to see if grade is empty for reset
if (whatIf == null) {
assignment.submission = null
- recyclerAdapter.assignmentsHash[assignment.id]?.submission = null
+ recyclerAdapter?.assignmentsHash?.get(assignment.id)?.submission = null
} else {
- recyclerAdapter.assignmentsHash[assignment.id]?.submission = Submission(
+ recyclerAdapter?.assignmentsHash?.get(assignment.id)?.submission = Submission(
score = whatIf,
grade = whatIf.toString()
)
@@ -122,14 +122,17 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
)
view.let {
configureViews(it)
- configureRecyclerView(it, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.gradesEmptyView, R.id.listView)
+ recyclerAdapter?.let {recyclerAdapter ->
+ configureRecyclerView(it, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.gradesEmptyView, R.id.listView)
+ }
+
}
}
override fun onDestroyView() {
super.onDestroyView()
computeGradesJob?.cancel()
- recyclerAdapter.cancel()
+ recyclerAdapter?.cancel()
}
override fun applyTheme() {
@@ -143,7 +146,10 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- view?.let { configureRecyclerView(it, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.gradesEmptyView, R.id.listView) }
+ view?.let {
+ recyclerAdapter?.let { recyclerAdapter ->
+ configureRecyclerView(it, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.gradesEmptyView, R.id.listView) }
+ }
}
private fun configureViews(rootView: View) {
@@ -169,7 +175,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
computeGrades(showTotalCheckBox.isChecked, -1)
} else {
val gradeString = getGradeString(
- recyclerAdapter.courseGrade,
+ recyclerAdapter?.courseGrade,
!isChecked
)
txtOverallGrade.text = gradeString
@@ -182,24 +188,26 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
whatIfView.setOnClickListener { showWhatIfCheckBox.toggle() }
showWhatIfCheckBox.setOnCheckedChangeListener { _, _ ->
- val currentScoreVal = recyclerAdapter.courseGrade?.currentScore ?: 0.0
+ val currentScoreVal = recyclerAdapter?.courseGrade?.currentScore ?: 0.0
val currentScore = NumberHelper.doubleToPercentage(currentScoreVal)
if (!showWhatIfCheckBox.isChecked) {
txtOverallGrade.text = currentScore
- } else if (recyclerAdapter.whatIfGrade != null) {
- txtOverallGrade.text = NumberHelper.doubleToPercentage(recyclerAdapter.whatIfGrade)
+ } else if (recyclerAdapter?.whatIfGrade != null) {
+ recyclerAdapter?.let {
+ txtOverallGrade.text = NumberHelper.doubleToPercentage(it.whatIfGrade)
+ }
}
// If the user is turning off what if grades we need to do a full refresh, should be
// cached data, so fast.
if (!showWhatIfCheckBox.isChecked) {
- recyclerAdapter.whatIfGrade = null
- recyclerAdapter.loadCachedData()
+ recyclerAdapter?.whatIfGrade = null
+ recyclerAdapter?.loadCachedData()
} else {
// Only log when what if grades is checked on
Analytics.logEvent(AnalyticsEventConstants.WHAT_IF_GRADES)
- recyclerAdapter.notifyDataSetChanged()
+ recyclerAdapter?.notifyDataSetChanged()
}
}
}
@@ -270,12 +278,12 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
override fun onNothingSelected(parent: AdapterView<*>?) {}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
// The current item must always be set first
- recyclerAdapter.currentGradingPeriod = termAdapter?.getItem(position)
+ recyclerAdapter?.currentGradingPeriod = termAdapter?.getItem(position)
if (termAdapter?.getItem(position)?.title == getString(R.string.allGradingPeriods)) {
- recyclerAdapter.loadData()
+ recyclerAdapter?.loadData()
} else {
if (termAdapter?.isEmpty == false) {
- recyclerAdapter.loadAssignmentsForGradingPeriod(
+ recyclerAdapter?.loadAssignmentsForGradingPeriod(
gradingPeriodID = termAdapter?.getItem(position)?.id.orDefault(),
refreshFirst = true,
forceNetwork = true
@@ -290,8 +298,10 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
}
// If we have a "current" grading period select it
- if (recyclerAdapter.currentGradingPeriod != null) {
- val position = termAdapter?.getPositionForId(recyclerAdapter.currentGradingPeriod?.id ?: -1) ?: -1
+ if (recyclerAdapter?.currentGradingPeriod != null) {
+ val position = recyclerAdapter?.let {
+ termAdapter?.getPositionForId(it.currentGradingPeriod?.id ?: -1) ?: -1
+ } ?: -1
if (position != -1) {
termSpinner.setSelection(position)
} else {
@@ -335,7 +345,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
}
private fun lockGrade(isLocked: Boolean) {
- if (isLocked || recyclerAdapter.isAllGradingPeriodsSelected && !course.isTotalsForAllGradingPeriodsEnabled) {
+ if (isLocked || recyclerAdapter?.isAllGradingPeriodsSelected == true && !course.isTotalsForAllGradingPeriodsEnabled) {
binding.txtOverallGrade.setInvisible()
binding.lockedGradeImage.setVisible()
binding.gradeToggleView.setGone()
@@ -351,25 +361,27 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
private fun computeGrades(isShowTotalGrade: Boolean, lastPositionChanged: Int) {
computeGradesJob = weave {
val result = inBackground {
- if (!isShowTotalGrade) {
- if (course.isApplyAssignmentGroupWeights) {
- calcGradesTotal(recyclerAdapter.assignmentGroups)
- } else {
- calcGradesTotalNoWeight(recyclerAdapter.assignmentGroups)
- }
- } else { //Calculates grade based on only graded assignments
- if (course.isApplyAssignmentGroupWeights) {
- calcGradesGraded(recyclerAdapter.assignmentGroups)
- } else {
- calcGradesGradedNoWeight(recyclerAdapter.assignmentGroups)
+ recyclerAdapter?.let { recyclerAdapter ->
+ if (!isShowTotalGrade) {
+ if (course.isApplyAssignmentGroupWeights) {
+ calcGradesTotal(recyclerAdapter.assignmentGroups)
+ } else {
+ calcGradesTotalNoWeight(recyclerAdapter.assignmentGroups)
+ }
+ } else { //Calculates grade based on only graded assignments
+ if (course.isApplyAssignmentGroupWeights) {
+ calcGradesGraded(recyclerAdapter.assignmentGroups)
+ } else {
+ calcGradesGradedNoWeight(recyclerAdapter.assignmentGroups)
+ }
}
}
}
- recyclerAdapter.whatIfGrade = result
+ recyclerAdapter?.whatIfGrade = result
binding.txtOverallGrade.text = NumberHelper.doubleToPercentage(result)
- if(lastPositionChanged >= 0) recyclerAdapter.notifyItemChanged(lastPositionChanged)
+ if(lastPositionChanged >= 0) recyclerAdapter?.notifyItemChanged(lastPositionChanged)
}
}
@@ -392,7 +404,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
var totalPoints = 0.0
val weight = g.groupWeight
for (a in g.assignments) {
- val tempAssignment = recyclerAdapter.assignmentsHash[a.id].takeIf { !it?.omitFromFinalGrade.orDefault() }
+ val tempAssignment = recyclerAdapter?.assignmentsHash?.get(a.id).takeIf { !it?.omitFromFinalGrade.orDefault() }
val tempSub = tempAssignment?.submission
if (tempSub?.grade != null && tempAssignment.submissionTypesRaw.isNotEmpty()) {
earnedPoints += tempSub.score
@@ -427,7 +439,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
val weight = g.groupWeight
var assignCount = 0
for (a in g.assignments) {
- val tempAssignment = recyclerAdapter.assignmentsHash[a.id].takeIf { !it?.omitFromFinalGrade.orDefault() }
+ val tempAssignment = recyclerAdapter?.assignmentsHash?.get(a.id).takeIf { !it?.omitFromFinalGrade.orDefault() }
val tempSub = tempAssignment?.submission
if (tempSub?.grade != null && tempAssignment.submissionTypesRaw.isNotEmpty() && Const.PENDING_REVIEW != tempSub.workflowState) {
assignCount++ // Determines if a group contains assignments
@@ -476,7 +488,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
var totalPoints = 0.0
for (g in groups) {
for (a in g.assignments) {
- val tempAssignment = recyclerAdapter.assignmentsHash[a.id].takeIf { !it?.omitFromFinalGrade.orDefault() }
+ val tempAssignment = recyclerAdapter?.assignmentsHash?.get(a.id).takeIf { !it?.omitFromFinalGrade.orDefault() }
val tempSub = tempAssignment?.submission
if (tempSub?.grade != null && tempAssignment.submissionTypesRaw.isNotEmpty() && Const.PENDING_REVIEW != tempSub.workflowState) {
earnedPoints += tempSub.score
@@ -510,7 +522,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable {
var earnedPoints = 0.0
for (g in groups) {
for (a in g.assignments) {
- val tempAssignment = recyclerAdapter.assignmentsHash[a.id].takeIf { !it?.omitFromFinalGrade.orDefault() }
+ val tempAssignment = recyclerAdapter?.assignmentsHash?.get(a.id).takeIf { !it?.omitFromFinalGrade.orDefault() }
val tempSub = tempAssignment?.submission
if (tempSub?.grade != null && tempAssignment.submissionTypesRaw.isNotEmpty()) {
totalPoints += tempAssignment.pointsPossible
diff --git a/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRecyclerAdapter.kt
index bc534a568d..a0cd507ec8 100644
--- a/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRecyclerAdapter.kt
@@ -21,11 +21,6 @@ import android.content.Context
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
-import com.instructure.canvasapi2.StatusCallback
-import com.instructure.canvasapi2.managers.AssignmentManager
-import com.instructure.canvasapi2.managers.CourseManager
-import com.instructure.canvasapi2.managers.EnrollmentManager
-import com.instructure.canvasapi2.managers.SubmissionManager
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.AssignmentGroup
import com.instructure.canvasapi2.models.CanvasContext
@@ -33,9 +28,7 @@ import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.CourseGrade
import com.instructure.canvasapi2.models.Enrollment
import com.instructure.canvasapi2.models.GradingPeriod
-import com.instructure.canvasapi2.models.GradingPeriodResponse
import com.instructure.canvasapi2.models.GradingSchemeRow
-import com.instructure.canvasapi2.models.Submission
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.isNullOrEmpty
import com.instructure.pandarecycler.util.GroupSortedList
@@ -117,7 +110,7 @@ open class GradesListRecyclerAdapter(
}
fun loadCachedData() {
- adapterToRecyclerViewCallback.refresh()
+ adapterToRecyclerViewCallback?.refresh()
resetData()
loadData(false)
}
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/modules/list/ModuleListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListFragment.kt
index 134ea38aea..1d9e96862c 100644
--- a/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListFragment.kt
@@ -64,7 +64,7 @@ class ModuleListFragment : ParentFragment(), Bookmarkable {
private lateinit var recyclerBinding: PandaRecyclerRefreshLayoutBinding
private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT)
- private lateinit var recyclerAdapter: ModuleListRecyclerAdapter
+ private var recyclerAdapter: ModuleListRecyclerAdapter? = null
@Inject
lateinit var repository: ModuleListRepository
@@ -85,7 +85,7 @@ class ModuleListFragment : ParentFragment(), Bookmarkable {
}
override fun onDestroy() {
- recyclerAdapter.cancel()
+ recyclerAdapter?.cancel()
super.onDestroy()
}
@@ -105,7 +105,7 @@ class ModuleListFragment : ParentFragment(), Bookmarkable {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- if (recyclerAdapter.size() == 0) {
+ if (recyclerAdapter?.size() == 0) {
recyclerBinding.emptyView.changeTextSize()
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
if (isTablet) {
@@ -160,9 +160,9 @@ class ModuleListFragment : ParentFragment(), Bookmarkable {
if (isLocked) return
// Remove all the subheaders and stuff.
- val groups = recyclerAdapter.groups
+ val groups = recyclerAdapter?.groups ?: arrayListOf()
- val moduleItemsArray = groups.indices.mapTo(ArrayList()) { recyclerAdapter.getItems(groups[it]) }
+ val moduleItemsArray = groups.indices.mapTo(ArrayList()) { recyclerAdapter?.getItems(groups[it]) ?: arrayListOf() }
val moduleHelper = ModuleProgressionUtility.prepareModulesForCourseProgression(
requireContext(), moduleItem.id, groups, moduleItemsArray
)
@@ -184,16 +184,16 @@ class ModuleListFragment : ParentFragment(), Bookmarkable {
// We need to force the empty view to be visible to use it for errors on refresh
recyclerBinding.emptyView.setVisible()
setEmptyView(recyclerBinding.emptyView, R.drawable.ic_panda_nomodules, R.string.modulesLocked, R.string.modulesLockedSubtext)
- } else if (recyclerAdapter.size() == 0) {
+ } else if (recyclerAdapter?.size() == 0) {
setEmptyView(recyclerBinding.emptyView, R.drawable.ic_panda_nomodules, R.string.noModules, R.string.noModulesSubtext)
} else if (!arguments?.getString(MODULE_ID).isNullOrEmpty()) {
// We need to delay scrolling until the expand animation has completed, otherwise modules
// that appear near the end of the list will not have the extra 'expanded' space needed
// to scroll as far as possible toward the top
recyclerBinding.listView.postDelayed({
- val groupPosition = recyclerAdapter.getGroupItemPosition(arguments!!.getString(
+ val groupPosition = recyclerAdapter?.getGroupItemPosition(arguments!!.getString(
MODULE_ID
- )!!.toLong())
+ )!!.toLong()) ?: -1
if (groupPosition >= 0) {
val lm = recyclerBinding.listView.layoutManager as? LinearLayoutManager
lm?.scrollToPositionWithOffset(groupPosition, 0)
@@ -203,22 +203,24 @@ class ModuleListFragment : ParentFragment(), Bookmarkable {
}
}
})
- configureRecyclerView(requireView(), requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView)
+ recyclerAdapter?.let {
+ configureRecyclerView(requireView(), requireContext(), it, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView)
+ }
}
fun notifyOfItemChanged(`object`: ModuleObject?, item: ModuleItem?) {
if (item == null || `object` == null) return
- recyclerAdapter.addOrUpdateItem(`object`, item)
+ recyclerAdapter?.addOrUpdateItem(`object`, item)
}
- fun refreshModuleList() = recyclerAdapter.updateMasteryPathItems()
+ fun refreshModuleList() = recyclerAdapter?.updateMasteryPathItems()
/**
* Update the list without clearing the data or collapsing headers. Used to update possibly updated
* items (like a page that has now been viewed)
*/
- private fun updateList(moduleObject: ModuleObject) = recyclerAdapter.updateWithoutResettingViews(moduleObject)
+ private fun updateList(moduleObject: ModuleObject) = recyclerAdapter?.updateWithoutResettingViews(moduleObject)
// region Bus Events
@@ -227,7 +229,7 @@ class ModuleListFragment : ParentFragment(), Bookmarkable {
fun onModuleUpdated(event: ModuleUpdatedEvent) {
event.once(javaClass.simpleName) {
updateList(it)
- recyclerAdapter.notifyDataSetChanged()
+ recyclerAdapter?.notifyDataSetChanged()
}
}
// endregion
diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt
index 9bf146ebba..7338f5d9a9 100644
--- a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt
@@ -127,8 +127,8 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- binding.prevItem.background = ColorKeeper.getColoredDrawable(requireActivity(), R.drawable.ic_chevron_left, canvasContext.textAndIconColor)
- binding.nextItem.background = ColorKeeper.getColoredDrawable(requireActivity(), R.drawable.ic_chevron_right, canvasContext.textAndIconColor)
+ binding.prevItem.setImageDrawable(ColorKeeper.getColoredDrawable(requireActivity(), R.drawable.ic_chevron_left, canvasContext.textAndIconColor))
+ binding.nextItem.setImageDrawable(ColorKeeper.getColoredDrawable(requireActivity(), R.drawable.ic_chevron_right, canvasContext.textAndIconColor))
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt
index 4a3bfe25ac..fdea8d6c99 100644
--- a/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt
@@ -56,11 +56,7 @@ object ModuleUtility {
syncedFileIds: List,
context: Context
): Fragment? = when (item.type) {
- "Page" -> {
- createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context, setOf(Tab.PAGES_ID)) {
- PageDetailsFragment.newInstance(PageDetailsFragment.makeRoute(course, item.title, item.pageUrl, navigatedFromModules))
- }
- }
+ "Page" -> PageDetailsFragment.newInstance(PageDetailsFragment.makeRoute(course, item.title, item.pageUrl, navigatedFromModules))
"Assignment" -> {
createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context, setOf(Tab.ASSIGNMENTS_ID, Tab.GRADES_ID, Tab.SYLLABUS_ID)) {
AssignmentDetailsFragment.newInstance(makeRoute(course, getAssignmentId(item)))
@@ -78,10 +74,8 @@ object ModuleUtility {
"Locked" -> LockedModuleItemFragment.newInstance(LockedModuleItemFragment.makeRoute(course, item.title!!, item.moduleDetails?.lockExplanation ?: ""))
"SubHeader" -> null // Don't do anything with headers, they're just dividers so we don't show them here.
"Quiz" -> {
- createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context, setOf(Tab.QUIZZES_ID)) {
- val apiURL = removeDomain(item.url)
- ModuleQuizDecider.newInstance(ModuleQuizDecider.makeRoute(course, item.htmlUrl!!, apiURL!!, item.contentId))
- }
+ val apiURL = removeDomain(item.url)
+ ModuleQuizDecider.newInstance(ModuleQuizDecider.makeRoute(course, item.htmlUrl!!, apiURL!!, item.contentId))
}
"ChooseAssignmentGroup" -> {
createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context) {
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 811db00ae2..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
@@ -62,7 +62,7 @@ class PageListFragment : ParentFragment(), Bookmarkable {
private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT)
- private lateinit var recyclerAdapter: PageListRecyclerAdapter
+ private var recyclerAdapter: PageListRecyclerAdapter? = null
private var defaultSelectedPageTitle = PageListRecyclerAdapter.FRONT_PAGE_DETERMINER // blank string is used to determine front page
private var isShowFrontPage by BooleanArg(key = SHOW_FRONT_PAGE)
@@ -73,7 +73,7 @@ class PageListFragment : ParentFragment(), Bookmarkable {
@Subscribe
fun onUpdatePage(event: PageUpdatedEvent) {
event.once(javaClass.simpleName) {
- recyclerAdapter.refresh()
+ recyclerAdapter?.refresh()
}
}
@@ -88,7 +88,7 @@ class PageListFragment : ParentFragment(), Bookmarkable {
}
override fun onDestroyView() {
- recyclerAdapter.cancel()
+ recyclerAdapter?.cancel()
super.onDestroyView()
}
@@ -120,16 +120,17 @@ class PageListFragment : ParentFragment(), Bookmarkable {
}
}, defaultSelectedPageTitle)
- configureRecyclerView(rootView!!, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView)
+ recyclerAdapter?.let {
+ configureRecyclerView(rootView!!, requireContext(), it, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView)
+ }
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
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)
}
@@ -137,7 +138,9 @@ class PageListFragment : ParentFragment(), Bookmarkable {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- configureRecyclerView(rootView!!, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView)
+ recyclerAdapter?.let {
+ configureRecyclerView(rootView!!, requireContext(), it, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView)
+ }
recyclerBinding.emptyView.changeTextSize()
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
if (isTablet) {
@@ -172,7 +175,7 @@ class PageListFragment : ParentFragment(), Bookmarkable {
} else {
recyclerBinding.emptyView.emptyViewText(getString(R.string.noItemsMatchingQuery, query))
}
- recyclerAdapter.searchQuery = query
+ recyclerAdapter?.searchQuery = query
}
ViewStyler.themeToolbarColored(requireActivity(), toolbar, canvasContext)
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListRecyclerAdapter.kt
index 12b3ce5d5d..a759e1c3e3 100644
--- a/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListRecyclerAdapter.kt
@@ -109,7 +109,7 @@ open class PageListRecyclerAdapter(
onCallbackFinished()
} catch {
if (itemCount == 0 || !APIHelper.hasNetworkConnection()) {
- adapterToRecyclerViewCallback.setIsEmpty(true)
+ adapterToRecyclerViewCallback?.setIsEmpty(true)
} else {
context.toast(R.string.errorOccurred)
}
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/features/quiz/list/QuizListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListRecyclerAdapter.kt
index c65219affd..24988615b6 100644
--- a/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListRecyclerAdapter.kt
@@ -90,7 +90,7 @@ class QuizListRecyclerAdapter(
}
adapterToFragmentCallback?.onRefreshFinished()
- if (size() == 0) adapterToRecyclerViewCallback.setIsEmpty(true)
+ if (size() == 0) adapterToRecyclerViewCallback?.setIsEmpty(true)
notifyDataSetChanged()
}
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt
index 594680ba04..3415885946 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt
@@ -28,7 +28,6 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.instructure.canvasapi2.utils.*
import com.instructure.canvasapi2.utils.pageview.PageView
-import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog
import com.instructure.pandautils.analytics.SCREEN_VIEW_APPLICATION_SETTINGS
import com.instructure.pandautils.analytics.ScreenView
import com.instructure.pandautils.binding.viewBinding
@@ -182,11 +181,8 @@ class ApplicationSettingsFragment : ParentFragment() {
private fun setUpSyncSettings() {
lifecycleScope.launch {
- if (!featureFlagProvider.offlineEnabled()) {
- binding.offlineContentDivider.setGone()
- binding.offlineContentTitle.setGone()
- binding.offlineSyncSettingsContainer.setGone()
- } else {
+ val offlineEnabled = (activity as? SettingsActivity)?.offlineEnabled ?: false
+ if (offlineEnabled) {
syncSettingsFacade.getSyncSettingsListenable().observe(viewLifecycleOwner) { syncSettings ->
if (syncSettings == null) {
binding.offlineSyncSettingsContainer.setGone()
@@ -202,6 +198,10 @@ class ApplicationSettingsFragment : ParentFragment() {
binding.offlineSyncSettingsContainer.onClick {
addFragment(SyncSettingsFragment.newInstance())
}
+ } else {
+ binding.offlineContentDivider.setGone()
+ binding.offlineContentTitle.setGone()
+ binding.offlineSyncSettingsContainer.setGone()
}
}
}
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 2bfd905eaf..9498e39d20 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,17 +25,23 @@ 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
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.ItemTouchHelper
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.*
-import com.instructure.canvasapi2.utils.APIHelper
import com.instructure.canvasapi2.utils.pageview.PageView
import com.instructure.canvasapi2.utils.weave.awaitApi
import com.instructure.canvasapi2.utils.weave.catch
@@ -44,9 +50,12 @@ import com.instructure.interactions.router.Route
import com.instructure.pandautils.analytics.SCREEN_VIEW_DASHBOARD
import com.instructure.pandautils.analytics.ScreenView
import com.instructure.pandautils.binding.viewBinding
+import com.instructure.pandautils.features.dashboard.DashboardCourseItem
import com.instructure.pandautils.features.dashboard.edit.EditDashboardFragment
import com.instructure.pandautils.features.dashboard.notifications.DashboardNotificationsFragment
import com.instructure.pandautils.features.offline.offlinecontent.OfflineContentFragment
+import com.instructure.pandautils.features.offline.sync.AggregateProgressObserver
+import com.instructure.pandautils.features.offline.sync.OfflineSyncWorker
import com.instructure.pandautils.utils.*
import com.instructure.student.R
import com.instructure.student.adapter.DashboardRecyclerAdapter
@@ -54,13 +63,14 @@ import com.instructure.student.databinding.CourseGridRecyclerRefreshLayoutBindin
import com.instructure.student.databinding.FragmentCourseGridBinding
import com.instructure.student.decorations.VerticalGridSpacingDecoration
import com.instructure.student.dialog.ColorPickerDialog
-import com.instructure.student.dialog.EditCourseNicknameDialog
+import com.instructure.pandautils.dialogs.EditCourseNicknameDialog
import com.instructure.student.events.CoreDataFinishedLoading
import com.instructure.student.events.CourseColorOverlayToggledEvent
import com.instructure.student.events.ShowGradesToggledEvent
import com.instructure.student.features.coursebrowser.CourseBrowserFragment
import com.instructure.student.features.dashboard.DashboardRepository
import com.instructure.student.flutterChannels.FlutterComm
+import com.instructure.student.holders.CourseViewHolder
import com.instructure.student.interfaces.CourseAdapterToFragmentCallback
import com.instructure.student.router.RouteMatcher
import com.instructure.student.util.StudentPrefs
@@ -86,6 +96,15 @@ class DashboardFragment : ParentFragment() {
@Inject
lateinit var networkStateProvider: NetworkStateProvider
+ @Inject
+ lateinit var aggregateProgressObserver: AggregateProgressObserver
+
+ @Inject
+ lateinit var workManager: WorkManager
+
+ @Inject
+ lateinit var firebaseCrashlytics: FirebaseCrashlytics
+
private val binding by viewBinding(FragmentCourseGridBinding::bind)
private lateinit var recyclerBinding: CourseGridRecyclerRefreshLayoutBinding
@@ -96,6 +115,8 @@ class DashboardFragment : ParentFragment() {
private var courseColumns: Int = LIST_SPAN_COUNT
private var groupColumns: Int = LIST_SPAN_COUNT
+ private val runningWorkers = mutableSetOf()
+
private val somethingChangedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (recyclerAdapter != null && intent?.extras?.getBoolean(Const.COURSE_FAVORITES) == true) {
@@ -120,6 +141,28 @@ class DashboardFragment : ParentFragment() {
recyclerAdapter?.refresh()
if (online) recyclerBinding.swipeRefreshLayout.isRefreshing = true
}
+
+ lifecycleScope.launch {
+ if (featureFlagProvider.offlineEnabled()) {
+ subscribeToOfflineSyncUpdates()
+ }
+ }
+ }
+
+ private fun subscribeToOfflineSyncUpdates() {
+ val workQuery = WorkQuery.Builder.fromTags(listOf(OfflineSyncWorker.PERIODIC_TAG, OfflineSyncWorker.ONE_TIME_TAG)).build()
+ workManager.getWorkInfosLiveData(workQuery).observe(this) { workInfos ->
+ workInfos.forEach { workInfo ->
+ if (workInfo.state == State.RUNNING) {
+ runningWorkers.add(workInfo.id.toString())
+ }
+ }
+
+ if (workInfos?.any { (it.state == State.SUCCEEDED || it.state == State.FAILED) && runningWorkers.contains(it.id.toString()) } == true) {
+ recyclerAdapter?.silentRefresh()
+ runningWorkers.clear()
+ }
+ }
}
@@ -237,16 +280,13 @@ class DashboardFragment : ParentFragment() {
}
recyclerBinding.listView.fadeAnimationWithAction {
- courseColumns = if (StudentPrefs.listDashboard) LIST_SPAN_COUNT else resources.getInteger(R.integer.course_card_columns)
- groupColumns = if (StudentPrefs.listDashboard) LIST_SPAN_COUNT else resources.getInteger(R.integer.group_card_columns)
- (recyclerBinding.listView.layoutManager as? GridLayoutManager)?.spanCount = courseColumns * groupColumns
- view?.post { recyclerAdapter?.notifyDataSetChanged() }
+ configureGridSize()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- configureRecyclerView()
+ configureGridSize()
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
if (isTablet) {
binding.emptyCoursesView.setGuidelines(.37f, .49f, .6f, .7f, .12f, .88f)
@@ -263,6 +303,16 @@ class DashboardFragment : ParentFragment() {
}
}
+ private fun configureGridSize() {
+ courseColumns =
+ if (StudentPrefs.listDashboard) LIST_SPAN_COUNT else resources.getInteger(R.integer.course_card_columns)
+ groupColumns =
+ if (StudentPrefs.listDashboard) LIST_SPAN_COUNT else resources.getInteger(R.integer.group_card_columns)
+ (recyclerBinding.listView.layoutManager as? GridLayoutManager)?.spanCount =
+ courseColumns * groupColumns
+ view?.post { recyclerAdapter?.notifyDataSetChanged() }
+ }
+
private fun configureRecyclerView() = with(binding) {
// Set up GridLayoutManager
courseColumns = if (StudentPrefs.listDashboard) LIST_SPAN_COUNT else resources.getInteger(R.integer.course_card_columns)
@@ -307,12 +357,103 @@ class DashboardFragment : ParentFragment() {
recyclerBinding.listView.clipToPadding = false
emptyCoursesView.onClickAddCourses {
- if (!APIHelper.hasNetworkConnection()) {
- toast(R.string.notAvailableOffline)
- } else {
- RouteMatcher.route(requireActivity(), EditDashboardFragment.makeRoute())
- }
+ RouteMatcher.route(requireActivity(), EditDashboardFragment.makeRoute())
}
+
+ 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,
+ 0
+ ) {
+
+ private var itemToMove: DashboardCourseItem? = null
+
+ // We need to consider other items in the recyclerview when dealing with positions.
+ // In the callbacks we get the adapter position, but we want to get the course items position so we use this.
+ // If there is any other recyclerview item added above the courses this should be modified.
+ private val POSITION_MODIFIER = 1
+
+ override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
+ super.onSelectedChanged(viewHolder, actionState)
+
+ if (viewHolder == null) return
+ val fromPosition = viewHolder.bindingAdapterPosition
+ val fromItem = recyclerAdapter?.getItem(DashboardRecyclerAdapter.ItemType.COURSE_HEADER, fromPosition - POSITION_MODIFIER) as? DashboardCourseItem
+
+ itemToMove = fromItem
+ }
+
+ override fun onMove(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder
+ ): Boolean {
+ val fromPosition = viewHolder.bindingAdapterPosition
+ val toPosition = target.bindingAdapterPosition
+
+ val itemsSize = recyclerAdapter?.getItems(DashboardRecyclerAdapter.ItemType.COURSE_HEADER)?.size ?: 0
+
+ if (toPosition - POSITION_MODIFIER in 0..< itemsSize) {
+ recyclerAdapter?.notifyItemMoved(fromPosition, toPosition)
+ }
+
+ return true
+ }
+
+ override fun getDragDirs(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder
+ ): Int {
+ return if (viewHolder is CourseViewHolder && networkStateProvider.isOnline()) {
+ ItemTouchHelper.START or ItemTouchHelper.END or ItemTouchHelper.DOWN or ItemTouchHelper.UP
+ } else 0
+ }
+
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
+
+ override fun clearView(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder
+ ) {
+ 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
+ }
+
+ val courseItems = recyclerAdapter?.getItems(DashboardRecyclerAdapter.ItemType.COURSE_HEADER)
+ ?.mapNotNull { it as? DashboardCourseItem } ?: emptyList()
+ val positions = courseItems
+ .mapIndexed { index, course -> Pair(course.course.contextId, index) }
+ .toMap()
+
+ val dashboardPositions = DashboardPositions(positions)
+ lifecycleScope.launch {
+ val updateResult = repository.updateDashboardPositions(dashboardPositions)
+ if (updateResult.isFail) {
+ toast(R.string.failedToUpdateDashboardOrder)
+ }
+ }
+ }
+ })
+
+ itemTouchHelper.attachToRecyclerView(recyclerBinding.listView)
}
override fun onStart() {
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt
index cdb1024802..d1adbbfd09 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt
@@ -65,19 +65,19 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager
private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT)
- private lateinit var recyclerAdapter: NotificationListRecyclerAdapter
+ private var recyclerAdapter: NotificationListRecyclerAdapter? = null
private var adapterToFragmentCallback: NotificationAdapterToFragmentCallback =
object : NotificationAdapterToFragmentCallback {
override fun onRowClicked(streamItem: StreamItem, position: Int, isOpenDetail: Boolean) {
- recyclerAdapter.setSelectedPosition(position)
+ recyclerAdapter?.setSelectedPosition(position)
onRowClick(streamItem)
}
override fun onRefreshFinished() {
setRefreshing(false)
binding.editOptions.setGone()
- if (recyclerAdapter.size() == 0) {
+ if (recyclerAdapter?.size() == 0) {
setEmptyView(recyclerBinding.emptyView, R.drawable.ic_panda_noalerts, R.string.noNotifications, R.string.noNotificationsSubtext)
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
recyclerBinding.emptyView.setGuidelines(.2f, .7f, .74f, .15f, .85f)
@@ -112,21 +112,23 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recyclerBinding = PandaRecyclerRefreshLayoutBinding.bind(binding.root)
recyclerAdapter = NotificationListRecyclerAdapter(requireContext(), canvasContext, adapterToFragmentCallback)
- configureRecyclerView(
- view,
- requireContext(),
- recyclerAdapter,
- R.id.swipeRefreshLayout,
- R.id.emptyView,
- R.id.listView
- )
+ recyclerAdapter?.let {
+ configureRecyclerView(
+ view,
+ requireContext(),
+ it,
+ R.id.swipeRefreshLayout,
+ R.id.emptyView,
+ R.id.listView
+ )
+ }
recyclerBinding.listView.isSelectionEnabled = false
binding.confirmButton.text = getString(R.string.delete)
- binding.confirmButton.setOnClickListener { recyclerAdapter.confirmButtonClicked() }
+ binding.confirmButton.setOnClickListener { recyclerAdapter?.confirmButtonClicked() }
binding.cancelButton.text = getString(R.string.cancel)
- binding.cancelButton.setOnClickListener { recyclerAdapter.cancelButtonClicked() }
+ binding.cancelButton.setOnClickListener { recyclerAdapter?.cancelButtonClicked() }
applyTheme()
@@ -139,14 +141,14 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager
if (activity?.supportFragmentManager?.fragments?.lastOrNull()?.javaClass == this.javaClass) {
if (shouldRefreshOnResume) {
recyclerBinding.swipeRefreshLayout.isRefreshing = true
- recyclerAdapter.refresh()
+ recyclerAdapter?.refresh()
shouldRefreshOnResume = false
}
}
}
override fun onDestroyView() {
- recyclerAdapter.cancel()
+ recyclerAdapter?.cancel()
activity?.supportFragmentManager?.removeOnBackStackChangedListener(this)
super.onDestroyView()
}
@@ -170,16 +172,18 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- configureRecyclerView(
- requireView(),
- requireContext(),
- recyclerAdapter,
- R.id.swipeRefreshLayout,
- R.id.emptyView,
- R.id.listView,
+ recyclerAdapter?.let {
+ configureRecyclerView(
+ requireView(),
+ requireContext(),
+ it,
+ R.id.swipeRefreshLayout,
+ R.id.emptyView,
+ R.id.listView,
R.string.noNotifications
- )
- if (recyclerAdapter.size() == 0) {
+ )
+ }
+ if (recyclerAdapter?.size() == 0) {
recyclerBinding.emptyView.changeTextSize()
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
if (isTablet) {
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt
index 4cbdbeb85e..24f868fa74 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt
@@ -60,6 +60,7 @@ import com.instructure.pandautils.utils.LoaderUtils
import com.instructure.pandautils.utils.PermissionUtils
import com.instructure.pandautils.utils.getDrawableCompat
import com.instructure.pandautils.utils.hasPermissions
+import com.instructure.pandautils.utils.toast
import com.instructure.pandautils.views.EmptyView
import com.instructure.student.R
import com.instructure.student.activity.VideoViewActivity
@@ -381,7 +382,12 @@ abstract class ParentFragment : DialogFragment(), FragmentInteractions, Navigati
}
onMainThread {
- LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID)
+ try {
+ LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID)
+ } catch (e: Exception) {
+ toast(R.string.unexpectedErrorOpeningFile)
+ onMediaLoadingComplete()
+ }
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt
index dc9b95e04f..e223a0b533 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt
@@ -51,11 +51,11 @@ class ToDoListFragment : ParentFragment() {
private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT)
- private lateinit var recyclerAdapter: TodoListRecyclerAdapter
+ private var recyclerAdapter: TodoListRecyclerAdapter? = null
private var adapterToFragmentCallback: NotificationAdapterToFragmentCallback = object : NotificationAdapterToFragmentCallback {
override fun onRowClicked(todo: ToDo, position: Int, isOpenDetail: Boolean) {
- recyclerAdapter.setSelectedPosition(position)
+ recyclerAdapter?.setSelectedPosition(position)
onRowClick(todo)
}
@@ -63,7 +63,7 @@ class ToDoListFragment : ParentFragment() {
if (!isAdded) return
setRefreshing(false)
binding.editOptions.setGone()
- if (recyclerAdapter.size() == 0) {
+ if (recyclerAdapter?.size() == 0) {
setEmptyView(recyclerViewBinding.emptyView, R.drawable.ic_panda_sleeping, R.string.noTodos, R.string.noTodosSubtext)
}
}
@@ -91,26 +91,28 @@ class ToDoListFragment : ParentFragment() {
}
}
recyclerAdapter = TodoListRecyclerAdapter(requireContext(), canvasContext, adapterToFragmentCallback)
- configureRecyclerView(
- view,
- requireContext(),
- recyclerAdapter,
- R.id.swipeRefreshLayout,
- R.id.emptyView,
- R.id.listView
- )
+ recyclerAdapter?.let {
+ configureRecyclerView(
+ view,
+ requireContext(),
+ it,
+ R.id.swipeRefreshLayout,
+ R.id.emptyView,
+ R.id.listView
+ )
+ }
recyclerViewBinding.listView.isSelectionEnabled = false
binding.confirmButton.text = getString(R.string.markAsDone)
- binding.confirmButton.setOnClickListener { recyclerAdapter.confirmButtonClicked() }
+ binding.confirmButton.setOnClickListener { recyclerAdapter?.confirmButtonClicked() }
binding.cancelButton.setText(R.string.cancel)
- binding.cancelButton.setOnClickListener { recyclerAdapter.cancelButtonClicked() }
+ binding.cancelButton.setOnClickListener { recyclerAdapter?.cancelButtonClicked() }
- updateFilterTitle(recyclerAdapter.getFilterMode())
+ updateFilterTitle(recyclerAdapter?.getFilterMode() ?: NoFilter)
binding.clearFilterTextView.setOnClickListener {
- recyclerAdapter.loadDataWithFilter(NoFilter)
- updateFilterTitle(recyclerAdapter.getFilterMode())
+ recyclerAdapter?.loadDataWithFilter(NoFilter)
+ updateFilterTitle(recyclerAdapter?.getFilterMode() ?: NoFilter)
}
}
@@ -132,16 +134,18 @@ class ToDoListFragment : ParentFragment() {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- configureRecyclerView(
- requireView(),
- requireContext(),
- recyclerAdapter,
- R.id.swipeRefreshLayout,
- R.id.emptyView,
- R.id.listView,
+ recyclerAdapter?.let {
+ configureRecyclerView(
+ requireView(),
+ requireContext(),
+ it,
+ R.id.swipeRefreshLayout,
+ R.id.emptyView,
+ R.id.listView,
R.string.noTodos
- )
- if (recyclerAdapter.size() == 0) {
+ )
+ }
+ if (recyclerAdapter?.size() == 0) {
recyclerViewBinding.emptyView.changeTextSize()
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
if (isTablet) {
@@ -164,12 +168,10 @@ class ToDoListFragment : ParentFragment() {
when {
toDo?.assignment != null -> { // Launch assignment details fragment.
if (toDo.assignment!!.discussionTopicHeader != null) {
- val groupTopic = toDo.assignment!!.discussionTopicHeader!!.groupTopicChildren.firstOrNull()
- if (groupTopic == null) { // Launch discussion details fragment
- RouteMatcher.route(requireActivity(), DiscussionRouterFragment.makeRoute(toDo.canvasContext!!, toDo.assignment!!.discussionTopicHeader!!))
- } else { // Launch discussion details fragment with the group
- RouteMatcher.route(requireActivity(), DiscussionRouterFragment.makeRoute(CanvasContext.emptyGroupContext(groupTopic.groupId), groupTopic.id))
- }
+ RouteMatcher.route(
+ requireActivity(),
+ DiscussionRouterFragment.makeRoute(toDo.canvasContext!!, toDo.assignment!!.discussionTopicHeader!!)
+ )
} else {
// Launch assignment details fragment.
RouteMatcher.route(requireActivity(), AssignmentDetailsFragment.makeRoute(toDo.canvasContext!!, toDo.assignment!!.id))
@@ -184,15 +186,15 @@ class ToDoListFragment : ParentFragment() {
private fun showCourseFilterDialog() {
val choices = arrayOf(getString(R.string.favoritedCoursesLabel))
- var checkedItem = choices.indexOf(getString(recyclerAdapter.getFilterMode().titleId))
+ var checkedItem = choices.indexOf(getString(recyclerAdapter?.getFilterMode()?.titleId ?: NoFilter.titleId))
val dialog = AlertDialog.Builder(requireContext())
.setTitle(R.string.filterByEllipsis)
.setSingleChoiceItems(choices, checkedItem) { _, index ->
checkedItem = index
}.setPositiveButton(android.R.string.ok) { _, _ ->
- if (checkedItem >= 0) recyclerAdapter.loadDataWithFilter(convertFilterChoiceToMode(choices[checkedItem]))
- updateFilterTitle(recyclerAdapter.getFilterMode())
+ if (checkedItem >= 0) recyclerAdapter?.loadDataWithFilter(convertFilterChoiceToMode(choices[checkedItem]))
+ updateFilterTitle(recyclerAdapter?.getFilterMode() ?: NoFilter)
}.setNegativeButton(android.R.string.cancel, null)
.create()
@@ -216,7 +218,7 @@ class ToDoListFragment : ParentFragment() {
override fun onDestroyView() {
super.onDestroyView()
- recyclerAdapter.cancel()
+ recyclerAdapter?.cancel()
}
companion object {
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ViewImageFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ViewImageFragment.kt
index e5c586603f..bce47bc595 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/ViewImageFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/ViewImageFragment.kt
@@ -91,7 +91,7 @@ class ViewImageFragment : Fragment(), ShareableFile {
private val requestListener = object : RequestListener {
- override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target?, p3: Boolean): Boolean {
+ override fun onLoadFailed(p0: GlideException?, p1: Any?, target: Target, p3: Boolean): Boolean {
binding.photoView.setGone()
binding.progressBar.setGone()
binding.errorContainer.setVisible()
@@ -100,11 +100,17 @@ class ViewImageFragment : Fragment(), ShareableFile {
return false
}
- override fun onResourceReady(drawable: Drawable?, p1: Any?, p2: Target?, p3: DataSource?, p4: Boolean): Boolean {
+ override fun onResourceReady(
+ resource: Drawable,
+ model: Any,
+ p2: Target?,
+ dataSource: DataSource,
+ p4: Boolean
+ ): Boolean {
binding.progressBar.setGone()
// Try to set the background color using palette if we can
- (drawable as? BitmapDrawable)?.bitmap?.let { colorBackground(it) }
+ (resource as? BitmapDrawable)?.bitmap?.let { colorBackground(it) }
return false
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/holders/GradeViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/GradeViewHolder.kt
index 2bd4e88500..4567300a2f 100644
--- a/apps/student/src/main/java/com/instructure/student/holders/GradeViewHolder.kt
+++ b/apps/student/src/main/java/com/instructure/student/holders/GradeViewHolder.kt
@@ -99,7 +99,7 @@ class GradeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
date.setVisible(date.text.isNotBlank())
- if (assignment.isMissing() && !isEdit && assignment.submission?.grade == null) {
+ if (assignment.isMissing() && !isEdit) {
submissionState.text = context.getString(R.string.missingAssignment)
submissionState.setTextColor(ContextCompat.getColor(context, R.color.textDanger))
submissionState.setVisible()
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt
index f9018ce7f9..0c1c06eb89 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt
@@ -69,17 +69,22 @@ internal class SubmissionFilesHolder(view: View) : RecyclerView.ViewHolder(view)
thumbnail.setVisible(data.thumbnailUrl.isValid())
data.thumbnailUrl.validOrNull()?.let {
Glide.with(root.context).load(it).listener(object : RequestListener {
- override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean {
+ override fun onLoadFailed(
+ e: GlideException?,
+ model: Any?,
+ target: Target,
+ isFirstResource: Boolean
+ ): Boolean {
fileIcon.setVisible(true)
thumbnail.setVisible(false)
return false
}
override fun onResourceReady(
- resource: Drawable?,
- model: Any?,
+ resource: Drawable,
+ model: Any,
target: Target?,
- dataSource: DataSource?,
+ dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
return false
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellView.kt
index 49b796f7ad..b5a9df7835 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellView.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellView.kt
@@ -67,6 +67,7 @@ class GradeCellView @JvmOverloads constructor(
pointsLabel.setVisible(state.showPointsLabel)
completeIcon.setVisible(state.showCompleteIcon)
incompleteIcon.setVisible(state.showIncompleteIcon)
+ yourGrade.setTextForVisibility(state.yourGrade)
latePenalty.setTextForVisibility(state.latePenalty)
finalGrade.setTextForVisibility(state.finalGrade)
grade.setTextForVisibility(state.grade)
@@ -75,7 +76,6 @@ class GradeCellView @JvmOverloads constructor(
outOf.contentDescription = state.outOfContentDescription
// Accent color
- latePenalty.setTextColor(state.accentColor)
chart.setColor(state.accentColor)
completeIcon.imageTintList = ColorStateList.valueOf(state.accentColor)
incompleteIcon.imageTintList = ColorStateList.valueOf(state.accentColor)
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellViewState.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellViewState.kt
index 42cf48e75c..f5f368b06a 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellViewState.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellViewState.kt
@@ -43,6 +43,7 @@ sealed class GradeCellViewState {
val gradeCellContentDescription: String = "",
val outOf: String = "",
val outOfContentDescription: String = "",
+ val yourGrade: String = "",
val latePenalty: String = "",
val finalGrade: String = "",
val stats: GradeStats? = null
@@ -132,8 +133,8 @@ sealed class GradeCellViewState {
)
}
- val score = NumberHelper.formatDecimal(submission.enteredScore, 2, true)
- val graphPercent = (submission.enteredScore / assignment.pointsPossible).coerceIn(0.0, 1.0).toFloat()
+ val score = NumberHelper.formatDecimal(submission.score, 2, true)
+ val graphPercent = (submission.score / assignment.pointsPossible).coerceIn(0.0, 1.0).toFloat()
// If grading type is Points, don't show the grade since we're already showing it as the score
var grade = if (assignment.gradingType != Assignment.POINTS_TYPE) submission.grade.orEmpty() else ""
@@ -148,12 +149,15 @@ sealed class GradeCellViewState {
var latePenalty = ""
var finalGrade = ""
+ var yourGrade = ""
// Adjust for late penalty, if any
- if (submission.pointsDeducted ?: 0.0 > 0.0) {
+ if ((submission.pointsDeducted ?: 0.0) > 0.0) {
grade = "" // Grade will be shown in the 'final grade' text
val pointsDeducted = NumberHelper.formatDecimal(submission.pointsDeducted!!, 2, true)
- latePenalty = context.getString(R.string.latePenalty, pointsDeducted)
+ val achievedScore = NumberHelper.formatDecimal(submission.enteredScore, 2, true)
+ yourGrade = context.getString(R.string.yourGrade, achievedScore)
+ latePenalty = context.getString(R.string.latePenaltyUpdated, pointsDeducted)
finalGrade = context.getString(R.string.finalGradeFormatted, submission.grade)
}
@@ -190,6 +194,7 @@ sealed class GradeCellViewState {
grade = grade,
gradeContentDescription = accessibleGradeString,
gradeCellContentDescription = gradeCellContentDescription,
+ yourGrade = yourGrade,
latePenalty = latePenalty,
finalGrade = finalGrade,
stats = stats
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt
index c1e5000be0..bd4b8b96b5 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt
@@ -18,7 +18,6 @@ package com.instructure.student.mobius.common.ui
import android.app.Activity
import android.content.Context
-import android.content.ContextWrapper
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -29,10 +28,24 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.instructure.canvasapi2.utils.Analytics
import com.instructure.interactions.FragmentInteractions
import com.instructure.interactions.Navigation
+import com.instructure.pandautils.utils.getFragmentActivity
import com.instructure.student.BuildConfig
import com.instructure.student.fragment.ParentFragment
-import com.instructure.student.mobius.common.*
-import com.spotify.mobius.*
+import com.instructure.student.mobius.common.ConsumerQueueWrapper
+import com.instructure.student.mobius.common.CoroutineConnection
+import com.instructure.student.mobius.common.GlobalEventMapper
+import com.instructure.student.mobius.common.GlobalEventSource
+import com.instructure.student.mobius.common.LateInit
+import com.instructure.student.mobius.common.MobiusExceptionLogger
+import com.instructure.student.mobius.common.contraMap
+import com.spotify.mobius.Connectable
+import com.spotify.mobius.Connection
+import com.spotify.mobius.EventSource
+import com.spotify.mobius.First
+import com.spotify.mobius.Init
+import com.spotify.mobius.Mobius
+import com.spotify.mobius.MobiusLoop
+import com.spotify.mobius.Update
import com.spotify.mobius.android.MobiusAndroid
import com.spotify.mobius.android.runners.MainThreadWorkRunner
import com.spotify.mobius.functions.Consumer
@@ -176,13 +189,7 @@ abstract class MobiusView(inflater: Lay
get() = parent.context
protected val activity: Activity
- get() = getActivity(context)
-
- private fun getActivity(context: Context): Activity {
- if (context is Activity) return context
- if (context is ContextWrapper) return getActivity(context.baseContext)
- else throw IllegalStateException("Not activity context")
- }
+ get() = context.getFragmentActivity()
abstract fun onConnect(output: Consumer)
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/navigation/StudentWebViewRouter.kt b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt
index 6eb681b270..7ae5299c74 100644
--- a/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt
+++ b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt
@@ -19,6 +19,7 @@ package com.instructure.student.navigation
import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.pandautils.navigation.WebViewRouter
+import com.instructure.student.fragment.InternalWebviewFragment
import com.instructure.student.router.RouteMatcher
class StudentWebViewRouter(val activity: FragmentActivity) : WebViewRouter {
@@ -34,4 +35,8 @@ class StudentWebViewRouter(val activity: FragmentActivity) : WebViewRouter {
override fun openMedia(url: String) {
RouteMatcher.openMedia(activity, url)
}
+
+ override fun routeExternally(url: String) {
+ RouteMatcher.route(activity, InternalWebviewFragment.makeRoute(url, url, false, ""))
+ }
}
\ No newline at end of file
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 ec829ffbc1..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
@@ -25,11 +25,11 @@ import com.heapanalytics.android.Heap
import com.instructure.canvasapi2.utils.ContextKeeper
import com.instructure.canvasapi2.utils.tryOrNull
import com.instructure.loginapi.login.tasks.LogoutTask
-import com.instructure.pandautils.features.offline.sync.CourseSyncWorker
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
@@ -40,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() {
@@ -79,8 +80,12 @@ class StudentLogoutTask(
override fun stopOfflineSync() {
val workManager = WorkManager.getInstance(ContextKeeper.appContext)
workManager.apply {
- cancelAllWorkByTag(CourseSyncWorker.TAG)
- cancelAllWorkByTag(OfflineSyncWorker.TAG)
+ cancelAllWorkByTag(OfflineSyncWorker.PERIODIC_TAG)
+ 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/BaseAppManager.kt b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt
index 51b573aaa4..09f8856ab6 100644
--- a/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt
@@ -49,6 +49,11 @@ import io.flutter.embedding.engine.dart.DartExecutor
abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), AnalyticsEventHandling, Configuration.Provider {
+ override val workManagerConfiguration: Configuration
+ get() = Configuration.Builder()
+ .setWorkerFactory(getWorkManagerFactory())
+ .build()
+
override fun onCreate() {
super.onCreate()
@@ -153,11 +158,6 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), Analyti
override fun performLogoutOnAuthError() = Unit
- override fun getWorkManagerConfiguration(): Configuration =
- Configuration.Builder()
- .setWorkerFactory(getWorkManagerFactory())
- .build()
-
abstract fun getWorkManagerFactory(): WorkerFactory
companion object {
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/StudentLogoutHelper.kt b/apps/student/src/main/java/com/instructure/student/util/StudentLogoutHelper.kt
new file mode 100644
index 0000000000..620eb7d127
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/util/StudentLogoutHelper.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ * 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.util
+
+import com.instructure.loginapi.login.tasks.LogoutTask
+import com.instructure.pandautils.room.offline.DatabaseProvider
+import com.instructure.pandautils.utils.LogoutHelper
+import com.instructure.student.tasks.StudentLogoutTask
+
+class StudentLogoutHelper : LogoutHelper {
+ override fun logout(databaseProvider: DatabaseProvider) {
+ StudentLogoutTask(LogoutTask.Type.LOGOUT, databaseProvider = databaseProvider).execute()
+ }
+}
\ 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/course_module_progression.xml b/apps/student/src/main/res/layout/course_module_progression.xml
index 4918489d1c..59ac4aa00f 100644
--- a/apps/student/src/main/res/layout/course_module_progression.xml
+++ b/apps/student/src/main/res/layout/course_module_progression.xml
@@ -147,23 +147,25 @@
-
+ android:contentDescription="@string/previous"
+ android:padding="16dp"
+ android:src="@drawable/ic_chevron_left" />
-
+ android:contentDescription="@string/next"
+ android:padding="16dp"
+ android:src="@drawable/ic_chevron_right" />
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/main/res/layout/view_student_enhanced_grade_cell.xml b/apps/student/src/main/res/layout/view_student_enhanced_grade_cell.xml
index fb89c22521..7eeb284f87 100644
--- a/apps/student/src/main/res/layout/view_student_enhanced_grade_cell.xml
+++ b/apps/student/src/main/res/layout/view_student_enhanced_grade_cell.xml
@@ -273,12 +273,27 @@
android:text="@{viewData.outOf}"
android:textColor="@color/textDarkest"
android:visibility="@{viewData.outOf.empty ? View.GONE : View.VISIBLE}"
- app:layout_constraintBottom_toTopOf="@id/latePenalty"
+ app:layout_constraintBottom_toTopOf="@id/yourGrade"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/chart"
app:layout_constraintTop_toBottomOf="@id/grade"
tools:text="Out of 100 pts" />
+
+
+ tools:text="89%" />
+ tools:text="Out of 100 pts" />
+
+
+ android:textColor="@color/textWarning"
+ tools:text="Late Penalty: -46 pts" />
+ tools:text="Final Grade: 89 pts" />
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 65c3352f1a..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,13 +24,23 @@ 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.*
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.CourseSettings
+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
@@ -38,8 +48,16 @@ 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 io.mockk.*
+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
+import io.mockk.unmockkAll
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
@@ -48,7 +66,8 @@ import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
-import java.util.*
+import java.util.Calendar
+import java.util.Date
@ExperimentalCoroutinesApi
class AssignmentDetailsViewModelTest {
@@ -69,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() {
@@ -85,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() {
@@ -99,6 +122,7 @@ class AssignmentDetailsViewModelTest {
colorKeeper,
application,
apiPrefs,
+ alarmScheduler,
database
)
@@ -723,4 +747,236 @@ class AssignmentDetailsViewModelTest {
Assert.assertTrue(viewModel.showContent(viewModel.state.value))
}
+
+ @Test
+ fun `Submit button is not visible when loaded as Observer`() {
+ val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Observer)))
+ 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?.submitVisible!!)
+ }
+
+ @Test
+ fun `Submit button is not visible when not between valid date range`() {
+ val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)), accessRestrictedByDate = true)
+ every { savedStateHandle.get(Const.CANVAS_CONTEXT) } returns course
+ 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?.submitVisible!!)
+ }
+
+ @Test
+ fun `Submit button is not visible when excused`() {
+ 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"), submission = Submission(excused = true))
+ coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment
+
+ val viewModel = getViewModel()
+
+ 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/dashboard/DashboardNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardNetworkDataSourceTest.kt
index 56eafb9f9b..94ea531de5 100644
--- a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardNetworkDataSourceTest.kt
@@ -18,6 +18,7 @@ package com.instructure.student.features.dashboard
import com.instructure.canvasapi2.apis.CourseAPI
import com.instructure.canvasapi2.apis.GroupAPI
+import com.instructure.canvasapi2.apis.UserAPI
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.DashboardCard
import com.instructure.canvasapi2.models.Group
@@ -37,8 +38,9 @@ class DashboardNetworkDataSourceTest {
private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true)
private val groupApi: GroupAPI.GroupInterface = mockk(relaxed = true)
private val apiPrefs: ApiPrefs = mockk(relaxed = true)
+ private val userApi: UserAPI.UsersInterface = mockk(relaxed = true)
- private val dataSource = DashboardNetworkDataSource(courseApi, groupApi, apiPrefs)
+ private val dataSource = DashboardNetworkDataSource(courseApi, groupApi, apiPrefs, userApi)
@Test
fun `getCourses returns courses for teacher if we are in the student view`() = runTest {
diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt
index dd8b0b4f5f..fcf03f3d66 100644
--- a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt
@@ -24,7 +24,6 @@ import com.instructure.pandautils.room.offline.daos.CourseDao
import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao
import com.instructure.pandautils.room.offline.entities.CourseEntity
import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import io.mockk.coEvery
@@ -120,8 +119,9 @@ class DashboardRepositoryTest {
coEvery { localDataSource.getDashboardCards(any()) } returns offlineCards
val result = repository.getDashboardCourses(true)
+ val expected = listOf(DashboardCard(3, position = 0), DashboardCard(4, position = 1))
- Assert.assertEquals(offlineCards, result)
+ Assert.assertEquals(expected, result)
}
@Test
@@ -134,13 +134,14 @@ class DashboardRepositoryTest {
coEvery { localDataSource.getDashboardCards(any()) } returns offlineCards
val result = repository.getDashboardCourses(true)
+ val expected = listOf(DashboardCard(1, position = 0), DashboardCard(2, position = 1))
- Assert.assertEquals(onlineCards, result)
+ Assert.assertEquals(expected, result)
}
@Test
fun `Returned dashboard courses are saved to the local store`() = runTest {
- val onlineCards = listOf(DashboardCard(1), DashboardCard(2))
+ val onlineCards = listOf(DashboardCard(1, position = 0), DashboardCard(2, position = 1))
every { networkStateProvider.isOnline() } returns true
coEvery { networkDataSource.getDashboardCards(any()) } returns onlineCards
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/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt
index 461a326251..0f60757041 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt
@@ -314,11 +314,13 @@ class GradeCellStateTest : Assert() {
score = 79.0
)
val expected = baseGradedState.copy(
- graphPercent = 0.85f,
- score = "85",
+ graphPercent = 0.79f,
+ score = "79",
showPointsLabel = true,
- latePenalty = "Late penalty (-6)",
- finalGrade = "Final Grade: 79"
+ yourGrade = "Your Grade: 85 pts",
+ latePenalty = "Late Penalty: -6 pts",
+ finalGrade = "Final Grade: 79",
+ gradeCellContentDescription = "79 Out of 100 points"
)
val actual = GradeCellViewState.fromSubmission(context, baseAssignment, submission)
assertEquals(expected, actual)
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsUpdateTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsUpdateTest.kt
index dba488f7e2..0686b245e1 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsUpdateTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsUpdateTest.kt
@@ -17,6 +17,7 @@ package com.instructure.student.test.assignment.details.submissionDetails
import android.net.Uri
import android.webkit.MimeTypeMap
+import android.webkit.URLUtil
import com.instructure.canvasapi2.models.*
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.DataResult
@@ -34,6 +35,7 @@ import com.spotify.mobius.test.NextMatchers.hasNothing
import com.spotify.mobius.test.UpdateSpec
import com.spotify.mobius.test.UpdateSpec.assertThatNext
import io.mockk.*
+import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
@@ -59,6 +61,14 @@ class SubmissionDetailsUpdateTest : Assert() {
submission = Submission(id = 30L, attempt = 1L, assignmentId = assignment.id)
initModel = SubmissionDetailsModel(assignmentId = assignment.id, canvasContext = course, isStudioEnabled = isStudioEnabled, assignmentEnhancementsEnabled = true)
ltiTool = LTITool(url = "https://www.instructure.com")
+
+ mockkStatic(URLUtil::class)
+ every { URLUtil.isNetworkUrl(any()) } returns true
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
}
@Test
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/commentTab/SubmissionCommentsUpdateTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/commentTab/SubmissionCommentsUpdateTest.kt
index f153bc0b2b..22ed89ffa7 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/commentTab/SubmissionCommentsUpdateTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/commentTab/SubmissionCommentsUpdateTest.kt
@@ -16,6 +16,7 @@
*/
package com.instructure.student.test.assignment.details.submissionDetails.commentTab
+import android.webkit.URLUtil
import com.instructure.canvasapi2.models.*
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsEffect
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsEvent
@@ -30,6 +31,10 @@ import com.spotify.mobius.test.NextMatchers.hasModel
import com.spotify.mobius.test.NextMatchers.hasNoModel
import com.spotify.mobius.test.UpdateSpec
import com.spotify.mobius.test.UpdateSpec.assertThatNext
+import io.mockk.every
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
@@ -51,6 +56,14 @@ class SubmissionCommentsUpdateTest : Assert() {
attemptId = 1,
assignmentEnhancementsEnabled = true
)
+
+ mockkStatic(URLUtil::class)
+ every { URLUtil.isNetworkUrl(any()) } returns false
+ }
+
+ @After
+ fun teardown() {
+ unmockkAll()
}
@Test
diff --git a/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt b/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt
index cf5fd6ed3c..25e4cae7c8 100644
--- a/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt
@@ -128,45 +128,6 @@ class ModuleUtilityTest : TestCase() {
TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString())
}
- @Test
- fun testGetFragment_page_offlineSynced() {
- val url = "https://mobile.canvas.net/api/v1/courses/222/pages/hello-world"
- val moduleItem = ModuleItem(
- id = 4567,
- type = "Page",
- url = url,
- title = "hello-world"
- )
-
- val course = Course()
- val expectedBundle = Bundle()
- expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course)
- expectedBundle.putString(PageDetailsFragment.PAGE_NAME, "hello-world")
- expectedBundle.putBoolean(PageDetailsFragment.NAVIGATED_FROM_MODULES, false)
-
- val parentFragment = callGetFragment(moduleItem, course, null, isOnline = false, tabs = setOf(Tab.PAGES_ID))
- TestCase.assertNotNull(parentFragment)
- TestCase.assertEquals(PageDetailsFragment::class.java, parentFragment!!.javaClass)
- TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString())
- }
-
- @Test
- fun testGetFragment_page_offlineNotSynced() {
- val url = "https://mobile.canvas.net/api/v1/courses/222/pages/hello-world"
- val moduleItem = ModuleItem(
- id = 4567,
- type = "Page",
- url = url,
- title = "hello-world"
- )
-
- val course = Course()
-
- val fragment = callGetFragment(moduleItem, course, null, isOnline = false)
- TestCase.assertNotNull(fragment)
- TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass)
- }
-
@Test
fun testGetFragment_assignment() {
val url = "https://mobile.canvas.net/api/v1/courses/222/assignments/123456789"
@@ -352,53 +313,6 @@ class ModuleUtilityTest : TestCase() {
TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString())
}
- @Test
- fun testGetFragment_quiz_offlineSynced() {
- val url = "https://mobile.canvas.net/api/v1/courses/222/quizzes/123456789"
- val htmlUrl = "https://mobile.canvas.net/courses/222/quizzes/123456789"
- val apiUrl = "courses/222/quizzes/123456789"
-
- val moduleItem = ModuleItem(
- id = 4567,
- type = "Quiz",
- url = url,
- htmlUrl = htmlUrl,
- contentId = 55
- )
-
- val course = Course()
- val expectedBundle = Bundle()
- expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course)
- expectedBundle.putString(Const.URL, htmlUrl)
- expectedBundle.putString(Const.API_URL, apiUrl)
- expectedBundle.putLong(Const.ID, 55)
-
- val parentFragment = callGetFragment(moduleItem, course, null, isOnline = false, tabs = setOf(Tab.QUIZZES_ID))
- TestCase.assertNotNull(parentFragment)
- TestCase.assertEquals(ModuleQuizDecider::class.java, parentFragment!!.javaClass)
- TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString())
- }
-
- @Test
- fun testGetFragment_quiz_offlineNotSynced() {
- val url = "https://mobile.canvas.net/api/v1/courses/222/quizzes/123456789"
- val htmlUrl = "https://mobile.canvas.net/courses/222/quizzes/123456789"
- val apiUrl = "courses/222/quizzes/123456789"
-
- val moduleItem = ModuleItem(
- id = 4567,
- type = "Quiz",
- url = url,
- htmlUrl = htmlUrl,
- contentId = 55
- )
-
- val course = Course()
- val fragment = callGetFragment(moduleItem, course, null, isOnline = false)
- TestCase.assertNotNull(fragment)
- TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass)
- }
-
@Test
fun testGetFragment_discussion() {
val url = "https://mobile.canvas.net/api/v1/courses/222/discussion_topics/123456789"
diff --git a/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt b/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt
index 5f57e39188..b3066f1c5c 100644
--- a/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt
@@ -28,7 +28,6 @@ import kotlinx.coroutines.runBlocking
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
-import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import org.hamcrest.Matcher
import org.hamcrest.Matchers
diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle
index 3ffda0139f..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 = 60
- versionName = '1.26.0'
+ 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
@@ -240,7 +249,6 @@ dependencies {
androidTestImplementation project(':espresso')
androidTestImplementation project(':dataseedingapi')
androidTestImplementation Libs.JUNIT
- androidTestImplementation project(path: ':panda_annotations')
androidTestUtil(Libs.TEST_ORCHESTRATOR) {
exclude module: 'support-annotations'
@@ -310,6 +318,10 @@ dependencies {
implementation Libs.ROOM
kapt Libs.ROOM_COMPILER
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/AnnouncementsListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AnnouncementsListPageTest.kt
index 64934a2262..33c7c1d89c 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AnnouncementsListPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AnnouncementsListPageTest.kt
@@ -19,6 +19,10 @@ package com.instructure.teacher.ui
import androidx.test.espresso.matcher.ViewMatchers.withId
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesCheckNames
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesViews
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse
@@ -27,10 +31,6 @@ import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer
import com.instructure.canvasapi2.models.CanvasContextPermission
import com.instructure.canvasapi2.models.Tab
import com.instructure.espresso.page.getStringFromResource
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.teacher.R
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.tokenLogin
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssigneeListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssigneeListPageTest.kt
index 2c84903a1b..925dede26e 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssigneeListPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssigneeListPageTest.kt
@@ -87,7 +87,7 @@ class AssigneeListPageTest : TeacherTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY))
val token = data.tokenFor(teacher)!!
tokenLogin(data.domain, token, teacher)
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 fa1f934da9..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
@@ -15,7 +15,11 @@
*/
package com.instructure.teacher.ui
-import com.instructure.canvas.espresso.mockCanvas.*
+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.addSubmissionForAssignment
+import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.Assignment.SubmissionType
@@ -28,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
@@ -38,45 +41,39 @@ import org.junit.Test
class AssignmentDetailsPageTest : TeacherTest() {
@Test
- @TestRail(ID = "C3109579")
override fun displaysPageObjects() {
getToAssignmentDetailsPage(
- submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY),
+ submissionTypes = listOf(ONLINE_TEXT_ENTRY),
students = 1,
withSubmission = true)
assignmentDetailsPage.assertPageObjects()
}
@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 = "C3134482")
fun displaysNoFromDate() {
val lockAt = 7.days.fromNow.iso8601
getToAssignmentDetailsPage(lockAt = lockAt)
@@ -84,7 +81,6 @@ class AssignmentDetailsPageTest : TeacherTest() {
}
@Test
- @TestRail(ID = "C3134483")
fun displaysNoToDate() {
getToAssignmentDetailsPage(unlockAt = 7.days.ago.iso8601)
assignmentDetailsPage.assertFromFilledAndToEmpty()
@@ -155,7 +151,7 @@ class AssignmentDetailsPageTest : TeacherTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = if(submissionTypes.isEmpty()) SubmissionType.ONLINE_TEXT_ENTRY else submissionTypes.first(),
+ submissionTypeList = submissionTypes.ifEmpty { listOf(ONLINE_TEXT_ENTRY) },
lockAt = lockAt,
unlockAt = unlockAt,
description = if(withDescription) Randomizer.randomCourseDescription() else "",
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 857ed54e7e..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()
@@ -77,7 +72,7 @@ class AssignmentDueDatesPageTest : TeacherTest() {
dueAt = dueAt,
lockAt = lockAt,
unlockAt = unlockAt,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
val token = data.tokenFor(teacher)!!
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 e65087514b..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()
@@ -89,7 +84,7 @@ class AssignmentListPageTest : TeacherTest() {
repeat(assignments) {
data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
assignmentGroupId = assignmentGroup.id)
}
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentSubmissionListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentSubmissionListPageTest.kt
index 271ce63154..09f2b53493 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentSubmissionListPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentSubmissionListPageTest.kt
@@ -15,7 +15,11 @@
*/
package com.instructure.teacher.ui
-import com.instructure.canvas.espresso.mockCanvas.*
+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.addSubmissionForAssignment
+import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CanvasContextPermission
import com.instructure.dataseeding.util.ago
@@ -132,7 +136,7 @@ class AssignmentSubmissionListPageTest : TeacherTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
dueAt = dueAt
)
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CommentLibraryPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CommentLibraryPageTest.kt
index 0ba9cc7115..3daf68f585 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CommentLibraryPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CommentLibraryPageTest.kt
@@ -17,16 +17,16 @@
package com.instructure.teacher.ui
import androidx.test.espresso.Espresso
+import com.instructure.canvas.espresso.FeatureCategory
+import com.instructure.canvas.espresso.Priority
+import com.instructure.canvas.espresso.TestCategory
+import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvas.espresso.mockCanvas.*
import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager
import com.instructure.canvasapi2.di.GraphQlApiModule
import com.instructure.canvasapi2.managers.CommentLibraryManager
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CanvasContextPermission
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.BindValue
@@ -255,7 +255,7 @@ class CommentLibraryPageTest : TeacherTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
data.addSubmissionForAssignment(
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 dd07dd63c1..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()
@@ -201,14 +197,12 @@ class EditAssignmentDetailsPageTest : TeacherTest() {
CanvasContextPermission() // Just need to have some sort of permissions object registered
)
-
val assignment = data.addAssignment(
courseId = course.id,
withDescription = withDescription,
lockAt = lockAt,
unlockAt = unlockAt,
- submissionType = submissionTypes.firstOrNull()
- ?: Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = submissionTypes.ifEmpty { listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY) }
)
val token = data.tokenFor(teacher)!!
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/EditSyllabusPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditSyllabusPageTest.kt
index ba7f29a88c..06aa6ac6d1 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditSyllabusPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditSyllabusPageTest.kt
@@ -90,7 +90,7 @@ class EditSyllabusPageTest : TeacherTest() {
data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
dueAt = 2.days.fromNow.iso8601,
name = "Assignment: 1"
)
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/InboxPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt
index 51b297cb5d..76439889cf 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt
@@ -18,6 +18,10 @@
package com.instructure.teacher.ui
import androidx.test.espresso.matcher.ViewMatchers
+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.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addConversation
import com.instructure.canvas.espresso.mockCanvas.addConversations
@@ -25,10 +29,6 @@ import com.instructure.canvas.espresso.mockCanvas.createBasicConversation
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Conversation
import com.instructure.canvasapi2.models.User
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.clickInboxTab
import com.instructure.teacher.ui.utils.tokenLogin
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..5bcdec9eab
--- /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.clickUpdateButton()
+
+ 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.clickUpdateButton()
+
+ 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.clickUpdateButton()
+
+ 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/SpeedGraderCommentsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderCommentsPageTest.kt
index f479a552ab..57634500cf 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderCommentsPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderCommentsPageTest.kt
@@ -15,8 +15,16 @@
*/
package com.instructure.teacher.ui
-import com.instructure.canvas.espresso.mockCanvas.*
-import com.instructure.canvasapi2.models.*
+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.addSubmissionsForAssignment
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.Attachment
+import com.instructure.canvasapi2.models.CanvasContextPermission
+import com.instructure.canvasapi2.models.Submission
+import com.instructure.canvasapi2.models.SubmissionComment
import com.instructure.espresso.randomString
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.tokenLogin
@@ -38,7 +46,7 @@ class SpeedGraderCommentsPageTest : TeacherTest() {
@Test
override fun displaysPageObjects() {
goToSpeedGraderCommentsPage(
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
speedGraderCommentsPage.assertPageObjects()
@@ -46,42 +54,42 @@ class SpeedGraderCommentsPageTest : TeacherTest() {
@Test
fun displaysAuthorName() {
- val submission = goToSpeedGraderCommentsPage(
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ val submissionList = goToSpeedGraderCommentsPage(
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
withComment = true
)
- val authorName = submission!!.submissionComments[0].authorName!!
+ val authorName = submissionList?.get(0)!!.submissionComments[0].authorName!!
speedGraderCommentsPage.assertDisplaysAuthorName(authorName)
}
@Test
fun displaysCommentText() {
- val submission = goToSpeedGraderCommentsPage(
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ val submissionList = goToSpeedGraderCommentsPage(
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
withComment = true
)
- val commentText = submission!!.submissionComments[0].comment!!
+ val commentText = submissionList?.get(0)!!.submissionComments[0].comment!!
speedGraderCommentsPage.assertDisplaysCommentText(commentText)
}
@Test
fun displaysCommentAttachment() {
- val submission = goToSpeedGraderCommentsPage(
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ val submissionList = goToSpeedGraderCommentsPage(
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
withComment = true,
attachment = attachment
)
- val attachment = submission!!.submissionComments[0].attachments.get(0)
+ val attachment = submissionList?.get(0)!!.submissionComments[0].attachments.get(0)
speedGraderCommentsPage.assertDisplaysCommentAttachment(attachment)
}
@Test
fun displaysSubmissionHistory() {
goToSpeedGraderCommentsPage(
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
speedGraderCommentsPage.assertDisplaysSubmission()
@@ -89,19 +97,19 @@ class SpeedGraderCommentsPageTest : TeacherTest() {
@Test
fun displaysSubmissionFile() {
- val submission = goToSpeedGraderCommentsPage(
- submissionType = Assignment.SubmissionType.ONLINE_UPLOAD,
+ val submissionList = goToSpeedGraderCommentsPage(
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_UPLOAD),
attachment = attachment
)
- val fileAttachments = submission!!.attachments.get(0)
+ val fileAttachments = submissionList?.get(0)!!.attachments[0]
speedGraderCommentsPage.assertDisplaysSubmissionFile(fileAttachments)
}
@Test
fun addsNewTextComment() {
goToSpeedGraderCommentsPage(
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
)
val newComment = randomString(32)
@@ -113,7 +121,7 @@ class SpeedGraderCommentsPageTest : TeacherTest() {
fun showsNoCommentsMessage() {
goToSpeedGraderCommentsPage(
submissionCount = 0,
- submissionType = Assignment.SubmissionType.ON_PAPER
+ submissionTypeList = listOf(Assignment.SubmissionType.ON_PAPER)
)
speedGraderCommentsPage.assertDisplaysEmptyState()
@@ -123,17 +131,17 @@ class SpeedGraderCommentsPageTest : TeacherTest() {
* Common setup routine
*
* [submissionCount] is the number of submissions for the created assignment. Typically 0 or 1.
- * [submissionType] is the submission type for the assignment.
+ * [submissionTypeList] is the submission type for the assignment.
* [withComment] if true, include a (student) comment with the submission.
* [attachment] if non-null, is either a comment attachment (if withComment is true) or a submission
* attachment (if withComment is false).
*
*/
private fun goToSpeedGraderCommentsPage(
- submissionCount: Int = 1,
- submissionType: Assignment.SubmissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
- withComment: Boolean = false,
- attachment: Attachment? = null): Submission? {
+ submissionCount: Int = 1,
+ submissionTypeList: List = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
+ withComment: Boolean = false,
+ attachment: Attachment? = null): MutableList? {
val data = MockCanvas.init(teacherCount = 1, studentCount = 1, courseCount = 1, favoriteCourseCount = 1)
val teacher = data.teachers[0]
@@ -147,7 +155,7 @@ class SpeedGraderCommentsPageTest : TeacherTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = submissionType
+ submissionTypeList = submissionTypeList
)
var submissionComment : SubmissionComment? = null
@@ -158,17 +166,19 @@ class SpeedGraderCommentsPageTest : TeacherTest() {
// Allows Espresso to distinguish between this and the full name, which is elsewhere on the page
authorName = student.shortName,
authorPronouns = student.pronouns,
+ attempt = 1L,
comment = "a comment",
attachments = if(attachment == null) arrayListOf() else arrayListOf(attachment)
)
}
- var submission: Submission? = null
+ var submissionList = mutableListOf()
repeat(submissionCount) {
- submission = data.addSubmissionForAssignment(
+ val submissionTypesRaw = submissionTypeList.map { it.apiString }
+ submissionList = data.addSubmissionsForAssignment(
assignmentId = assignment.id,
userId = student.id,
- type = submissionType.apiString,
+ types = submissionTypesRaw,
comment = submissionComment,
attachment = if (withComment) null else attachment
)
@@ -185,6 +195,6 @@ class SpeedGraderCommentsPageTest : TeacherTest() {
speedGraderPage.selectCommentsTab()
speedGraderPage.swipeUpCommentsTab()
- return submission
+ return submissionList
}
}
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderFilesPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderFilesPageTest.kt
index 929c21223e..f53301a282 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderFilesPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderFilesPageTest.kt
@@ -15,7 +15,11 @@
*/
package com.instructure.teacher.ui
-import com.instructure.canvas.espresso.mockCanvas.*
+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.addSubmissionForAssignment
+import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.Attachment
import com.instructure.canvasapi2.models.CanvasContextPermission
@@ -76,7 +80,7 @@ class SpeedGraderFilesPageTest : TeacherTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_UPLOAD
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_UPLOAD)
)
repeat(submissionCount) {
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt
index 5aa322958a..f743f88297 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt
@@ -187,7 +187,7 @@ class SpeedGraderGradePageTest : TeacherTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
pointsPossible = pointsPossible,
gradingType = gradingType
)
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderPageTest.kt
index 35e39d13f7..81a03fd8b6 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderPageTest.kt
@@ -15,9 +15,16 @@
*/
package com.instructure.teacher.ui
-import com.instructure.canvas.espresso.mockCanvas.*
+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.addSubmissionsForAssignment
+import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
-import com.instructure.canvasapi2.models.Assignment.SubmissionType.*
+import com.instructure.canvasapi2.models.Assignment.SubmissionType.EXTERNAL_TOOL
+import com.instructure.canvasapi2.models.Assignment.SubmissionType.ONLINE_TEXT_ENTRY
+import com.instructure.canvasapi2.models.Assignment.SubmissionType.ONLINE_URL
+import com.instructure.canvasapi2.models.Assignment.SubmissionType.ON_PAPER
import com.instructure.canvasapi2.models.CanvasContextPermission
import com.instructure.teacher.R
import com.instructure.teacher.ui.utils.TeacherTest
@@ -36,13 +43,13 @@ class SpeedGraderPageTest : TeacherTest() {
@Test
fun displaysSubmissionDropDown() {
- goToSpeedGraderPage(submissionType = ONLINE_TEXT_ENTRY, students = 1, submissions = listOf(2))
+ goToSpeedGraderPage(submissionTypeList = listOf(ONLINE_TEXT_ENTRY), students = 1, submissions = listOf(2))
speedGraderPage.assertHasSubmissionDropDown()
}
@Test
fun opensToCorrectSubmission() {
- val data = goToSpeedGraderPage(students = 4, submissionType = ONLINE_TEXT_ENTRY)
+ val data = goToSpeedGraderPage(students = 4, submissionTypeList = listOf(ONLINE_TEXT_ENTRY))
speedGraderPage.clickBackButton()
val students = data.students
for (i in 0 until students.size) {
@@ -73,37 +80,37 @@ class SpeedGraderPageTest : TeacherTest() {
@Test
fun displaysTextSubmission() {
- goToSpeedGraderPage(submissionType = ONLINE_TEXT_ENTRY, submissions = listOf(1))
+ goToSpeedGraderPage(submissionTypeList = listOf(ONLINE_TEXT_ENTRY), submissions = listOf(1))
speedGraderPage.assertDisplaysTextSubmissionView()
}
@Test
fun displaysUnsubmittedEmptyState() {
- goToSpeedGraderPage(submissionType = ONLINE_TEXT_ENTRY)
+ goToSpeedGraderPage(submissionTypeList = listOf(ONLINE_TEXT_ENTRY))
speedGraderPage.assertDisplaysEmptyState(R.string.noSubmissionTeacher)
}
@Test
fun displaysNoSubmissionsAllowedEmptyState() {
- goToSpeedGraderPage(submissionType = Assignment.SubmissionType.NONE)
+ goToSpeedGraderPage(submissionTypeList = listOf(Assignment.SubmissionType.NONE))
speedGraderPage.assertDisplaysEmptyState(R.string.speedGraderNoneMessage)
}
@Test
fun displaysOnPaperEmptyState() {
- goToSpeedGraderPage(submissionType = ON_PAPER)
+ goToSpeedGraderPage(submissionTypeList = listOf(ON_PAPER))
speedGraderPage.assertDisplaysEmptyState(R.string.speedGraderOnPaperMessage)
}
@Test
fun displaysExternalToolEmptyState() {
- goToSpeedGraderPage(submissionType = EXTERNAL_TOOL)
+ goToSpeedGraderPage(submissionTypeList = listOf(EXTERNAL_TOOL))
speedGraderPage.assertDisplaysEmptyState(R.string.noSubmissionTeacher)
}
@Test
fun displaysUrlSubmission() {
- val data = goToSpeedGraderPage(submissionType = ONLINE_URL, submissions = listOf(1))
+ val data = goToSpeedGraderPage(submissionTypeList = listOf(ONLINE_URL), submissions = listOf(1))
val assignment = data.assignments.values.first()
val submission = data.submissions[assignment.id]!!.first()
speedGraderPage.assertDisplaysUrlSubmissionLink(submission)
@@ -111,10 +118,10 @@ class SpeedGraderPageTest : TeacherTest() {
}
private fun goToSpeedGraderPage(
- students: Int = 1,
- submissionType: Assignment.SubmissionType = Assignment.SubmissionType.NONE,
- submissions: List = listOf(0),
- selectStudent: Int = 0
+ students: Int = 1,
+ submissionTypeList: List = listOf(Assignment.SubmissionType.NONE),
+ submissions: List = listOf(0),
+ selectStudent: Int = 0
): MockCanvas {
val data = MockCanvas.init(teacherCount = 1, studentCount = students, courseCount = 1, favoriteCourseCount = 1)
val teacher = data.teachers[0]
@@ -127,24 +134,24 @@ class SpeedGraderPageTest : TeacherTest() {
val assignment = data.addAssignment(
courseId = course.id,
- submissionType = submissionType
+ submissionTypeList = submissionTypeList
)
- val assignmentSubmissions =
- (0 until submissions.size).map {
- if(students < it + 1) throw Exception("student count does not agree with submissions")
- val student = data.students[it]
- val submissionCount = submissions[it]
- repeat(submissionCount) { index ->
- data.addSubmissionForAssignment(
- assignmentId = assignment.id,
- userId = student.id,
- type = submissionType.apiString,
- body = if(submissionType == Assignment.SubmissionType.ONLINE_URL) null else "AssignmentBody $index",
- url = if(submissionType == Assignment.SubmissionType.ONLINE_URL) "www.google.com" else null
- )
- }
- }
+ (0 until submissions.size).map {
+ if(students < it + 1) throw Exception("student count does not agree with submissions")
+ val student = data.students[it]
+ val submissionCount = submissions[it]
+ val submissionTypesRaw = submissionTypeList.map { it.apiString }
+ repeat(submissionCount) { index ->
+ data.addSubmissionsForAssignment(
+ assignmentId = assignment.id,
+ userId = student.id,
+ types = submissionTypesRaw,
+ body = if(submissionTypesRaw.contains(Assignment.SubmissionType.ONLINE_URL.apiString)) null else "AssignmentBody $index",
+ url = if(submissionTypesRaw.contains(Assignment.SubmissionType.ONLINE_URL.apiString)) "www.google.com" else null
+ )
+ }
+ }
val token = data.tokenFor(teacher)!!
tokenLogin(data.domain, token, teacher)
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt
index 50dc6a2d82..5cf04e871d 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt
@@ -16,7 +16,12 @@
*/
package com.instructure.teacher.ui
-import com.instructure.canvas.espresso.mockCanvas.*
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addAssignment
+import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent
+import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
+import com.instructure.canvas.espresso.mockCanvas.addCourseSettings
+import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CanvasContextPermission
import com.instructure.canvasapi2.models.CourseSettings
@@ -101,7 +106,7 @@ class SyllabusPageTest : TeacherTest() {
repeat(assignmentCount) {
data.addAssignment(
courseId = course.id,
- submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
dueAt = 2.days.fromNow.iso8601,
name = "Assignment: $it"
)
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 30257cff90..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
@@ -19,21 +19,17 @@ package com.instructure.teacher.ui.e2e
import android.util.Log
import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.seedData
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 95ff85feea..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
@@ -19,12 +19,12 @@ package com.instructure.teacher.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.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
@@ -33,10 +33,6 @@ import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
import com.instructure.espresso.assertContainsText
import com.instructure.espresso.page.onViewWithId
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.teacher.R
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.seedAssignmentSubmission
@@ -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 440681b7f6..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
@@ -18,21 +18,21 @@ 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.AssignmentsApi
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
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.seedData
import com.instructure.teacher.ui.utils.tokenLogin
@@ -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 d8b8a9080d..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
@@ -19,11 +19,11 @@ package com.instructure.teacher.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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.seedData
import com.instructure.teacher.ui.utils.tokenLogin
@@ -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 21dd220547..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
@@ -19,10 +19,10 @@ package com.instructure.teacher.ui.e2e
import android.util.Log
import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.seedData
import com.instructure.teacher.ui.utils.tokenLogin
@@ -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 7fed199a13..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
@@ -19,10 +19,10 @@ package com.instructure.teacher.ui.e2e
import android.util.Log
import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+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.seedData
import com.instructure.teacher.ui.utils.tokenLogin
@@ -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 87cc6c14c1..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
@@ -20,28 +20,22 @@ import android.os.Environment
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.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
import com.instructure.espresso.ViewUtils
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.seedData
import com.instructure.teacher.ui.utils.tokenLogin
@@ -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 f9ecf29cbc..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
@@ -4,14 +4,17 @@ import android.util.Log
import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers
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.ConversationsApi
import com.instructure.dataseeding.api.GroupsApi
import com.instructure.dataseeding.model.CanvasUserApiModel
import com.instructure.dataseeding.model.CourseApiModel
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+import com.instructure.espresso.retry
+import com.instructure.espresso.retryWithIncreasingDelay
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.seedData
import com.instructure.teacher.ui.utils.tokenLogin
@@ -20,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]
@@ -38,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)
@@ -77,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()
@@ -91,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"
@@ -168,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)
@@ -181,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()
@@ -218,7 +209,11 @@ class InboxE2ETest : TeacherTest() {
Log.d(STEP_TAG, "Select 'ARCHIVED' scope and assert that '${seedConversation2[0].subject}' conversation is displayed in the 'ARCHIVED' scope.")
inboxPage.filterMessageScope("Archived")
- inboxPage.assertConversationDisplayed(seedConversation2[0].subject)
+
+ retry(times = 10, delay = 3000, block = {
+ refresh()
+ inboxPage.assertConversationDisplayed(seedConversation2[0].subject)
+ })
Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and unarchive it." +
"Assert that the selected number of conversation on the toolbar is 1 and '${seedConversation2[0].subject}' conversation is not displayed in the 'ARCHIVED' scope.")
@@ -227,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")
@@ -296,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]
@@ -305,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)
@@ -318,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()
@@ -372,21 +365,28 @@ class InboxE2ETest : TeacherTest() {
Log.d(STEP_TAG, "Select both of the conversations. Star them and mark the unread.")
inboxPage.selectConversations(listOf(seedConversation2[0].subject, seedConversation3[0].subject))
- inboxPage.clickStar()
inboxPage.clickMarkAsRead()
+ retry(times = 10, delay = 3000, block = {
+ Log.d(STEP_TAG, "Assert that '${seedConversation3[0].subject}' conversation is read.")
+ inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.GONE)
+ })
+
+ Log.d(STEP_TAG, "Select both of the conversations. Star them and mark the unread.")
+ inboxPage.clickStar()
+
Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that both of the conversation are displayed in the 'STARRED' scope.")
inboxPage.filterMessageScope("Starred")
- inboxPage.assertConversationDisplayed(seedConversation2[0].subject)
- inboxPage.assertConversationDisplayed(seedConversation3[0].subject)
+
+ retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) {
+ inboxPage.assertConversationDisplayed(seedConversation2[0].subject)
+ inboxPage.assertConversationDisplayed(seedConversation3[0].subject)
+ }
Log.d(STEP_TAG, "Swipe '${seedConversation2[0].subject}' left and assert it is removed from the 'STARRED' scope because it has became unstarred.")
inboxPage.swipeConversationLeft(seedConversation2[0])
inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject)
- Log.d(STEP_TAG, "Assert that '${seedConversation3[0].subject}' conversation is read.")
- inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.GONE)
-
Log.d(STEP_TAG, "Swipe '${seedConversation3[0].subject}' conversation right and assert that it has became unread.")
inboxPage.swipeConversationRight(seedConversation3[0].subject)
inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.VISIBLE)
@@ -440,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 75374185c8..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
@@ -18,14 +18,14 @@ 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.SeedApi
import com.instructure.dataseeding.model.CanvasUserApiModel
import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.espresso.ViewUtils
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.seedData
import dagger.hilt.android.testing.HiltAndroidTest
@@ -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 7f9117fd15..daabbaaef9 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,37 +2,37 @@ 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
+import com.instructure.canvas.espresso.TestCategory
+import com.instructure.canvas.espresso.TestMetaData
import com.instructure.dataseeding.api.AssignmentsApi
import com.instructure.dataseeding.api.DiscussionTopicsApi
+import com.instructure.dataseeding.api.FileFolderApi
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.api.SubmissionsApi
+import com.instructure.dataseeding.model.FileUploadType
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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
-import com.instructure.teacher.ui.pages.WebViewTextCheck
-import com.instructure.teacher.ui.utils.TeacherTest
+import com.instructure.espresso.getCustomDateCalendar
+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 com.instructure.teacher.ui.utils.uploadTextFile
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 +47,552 @@ 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()
+ moduleListPage.assertEmptyView()
- Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.")
- val assignment = createAssignment(course, teacher)
+ 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 = createQuiz(course, teacher)
+ 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 = createCoursePage(course, teacher, published = false, frontPage = false, body = "
Test Page Text
")
+ Log.d(PREPARATION_TAG,"Create an unpublished page for course: '${course.name}'.")
+ val testPage = PagesApi.createCoursePage(course.id, teacher.token, published = false, body = "
Test Page Text
")
- Log.d(PREPARATION_TAG,"Create a discussion topic for ${course.name} course.")
- val discussionTopic = createDiscussion(course, teacher)
+ 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 = createModule(course, teacher)
+ 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 (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 '${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 ${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 '${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 ${discussionTopic.title} discussion with module: ${module.id}.")
- createModuleItem(course, module, teacher, discussionTopic.title, ModuleItemTypes.DISCUSSION.stringVal, discussionTopic.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(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(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,"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(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(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,"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(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(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, "Refresh the Modules Page.")
- modulesPage.refresh()
+ 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,"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, "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. Navigate back to Modules Page.")
- modulesPage.clickOnModuleItem(assignment.name)
+ 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, "Refresh the Module List Page.")
+ moduleListPage.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.")
+ moduleListPage.assertModuleItemIsDisplayed(assignment.name)
+ moduleListPage.assertModuleItemNotPublished(assignment.name)
+ moduleListPage.assertModuleItemIsDisplayed(quiz.title)
+ moduleListPage.assertModuleItemNotPublished(quiz.title)
+ moduleListPage.assertModuleItemIsDisplayed(testPage.title)
+ moduleListPage.assertModuleItemNotPublished(testPage.title)
+
+ Log.d(STEP_TAG, "Open the '${assignment.name}' assignment module item and assert that the Assignment Details Page is displayed")
+ moduleListPage.clickOnModuleItem(assignment.name)
+
+ Log.d(STEP_TAG, "Assert that the published status of the '${assignment.name}' assignment became 'Unpublished' on the Assignment Details Page.")
+ assignmentDetailsPage.assertPublishedStatus(false)
- 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"))
+ Log.d(STEP_TAG, "Open Edit Page of '${assignment.name}' assignment and publish it. Save the change.")
+ assignmentDetailsPage.openEditPage()
+ editAssignmentDetailsPage.clickPublishSwitch()
+ editAssignmentDetailsPage.saveAssignment()
+
+ 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)
+
+ 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)
+ }
+
+ @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 student = data.studentsList[0]
+ val course = data.coursesList[0]
- 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(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,"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(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 = "
Test Page Text
")
+
+ 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 = "
This is another test page
")
+
+ 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.assertModuleStatusIconAlpha(assignment.name, 0.5f)
+ 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 (except '${assignment.name}' assignment) became unpublished.")
+ moduleListPage.assertSnackbarText(R.string.allModulesAndAllItemsUnpublished)
+ moduleListPage.assertModuleNotPublished(module.name)
+ moduleListPage.assertModuleItemNotPublished(quiz.title)
+ moduleListPage.assertModuleItemNotPublished(testPage.title)
+ moduleListPage.assertModuleItemNotPublished(discussionTopic.title)
+
+ Log.d(STEP_TAG, "Assert that the '${assignment.name}' assignment remained published because it's unpublishable since it has a submission already.")
+ moduleListPage.assertModuleItemIsPublished(assignment.name)
+
+ 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.assertModuleItemIsPublished(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 (except '${assignment.name}' assignment) became unpublished.")
+ moduleListPage.assertSnackbarText(R.string.moduleAndAllItemsUnpublished)
+ moduleListPage.assertModuleNotPublished(module.name)
+ moduleListPage.assertModuleItemNotPublished(quiz.title)
+ moduleListPage.assertModuleItemNotPublished(testPage.title)
+ moduleListPage.assertModuleItemNotPublished(discussionTopic.title)
+
+ Log.d(STEP_TAG, "Assert that the '${assignment.name}' assignment remained published because it's unpublishable since it has a submission already.")
+ moduleListPage.assertModuleItemIsPublished(assignment.name)
+
+ 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 (except '${assignment.name}' assignment) remaining unpublished.")
+ moduleListPage.assertModuleIsPublished(module.name)
+ moduleListPage.assertModuleItemIsPublished(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 '${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, "Try to click on '${assignment.name}' assignment's overflow menu (in order to unpublish it). Assert that a snack bar with a proper text will be displayed that it cannot be unpublished since it has student submissions.")
+ moduleListPage.clickItemOverflow(assignment.name)
+ moduleListPage.assertSnackbarContainsText(assignment.name)
+ moduleListPage.assertModuleItemIsPublished(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)
}
- private fun createModuleItem(
- course: CourseApiModel,
- module: ModuleApiModel,
- teacher: CanvasUserApiModel,
- title: String,
- moduleItemType: String,
- contentId: String?,
- pageUrl: String? = null
- ) {
+ @E2E
+ @Test
+ @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.E2E)
+ fun testFileModuleItemUpdateE2E() {
+
+ 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 a module for '${course.name}' course. It starts as unpublished.")
+ val module = ModulesApi.createModule(course.id, teacher.token)
+
+ Log.d(PREPARATION_TAG, "Get the root folder of the course.")
+ val courseRootFolder = FileFolderApi.getCourseRootFolder(course.id, teacher.token)
+
+ Log.d(PREPARATION_TAG, "Create a (text) file within the root folder (so the 'Files' tab file list) of the '${course.name}' course.")
+ val testTextFile = uploadTextFile(courseRootFolder.id, token = teacher.token, fileUploadType = FileUploadType.COURSE_FILE)
+
+ Log.d(PREPARATION_TAG, "Create another (text) file within the root folder (so the 'Files' tab file list) of the '${course.name}' course.")
+ val testTextFile2 = uploadTextFile(courseRootFolder.id, token = teacher.token, fileUploadType = FileUploadType.COURSE_FILE)
+
+ Log.d(PREPARATION_TAG, "Associate '${testTextFile.fileName}' (course) file with module: '${module.id}'.")
ModulesApi.createModuleItem(
- courseId = course.id,
- moduleId = module.id,
- teacherToken = teacher.token,
- title = title,
- type = moduleItemType,
- contentId = contentId,
- pageUrl = pageUrl
+ course.id,
+ teacher.token,
+ module.id,
+ moduleItemTitle = testTextFile.fileName,
+ moduleItemType = ModuleItemTypes.FILE.stringVal,
+ contentId = testTextFile.id.toString()
)
- }
- private fun createModule(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ): ModuleApiModel {
- return ModulesApi.createModule(
- courseId = course.id,
- teacherToken = teacher.token,
- unlockAt = null
+ Log.d(PREPARATION_TAG, "Associate '${testTextFile2.fileName}' (course) file with module: '${module.id}'.")
+ ModulesApi.createModuleItem(
+ course.id,
+ teacher.token,
+ module.id,
+ moduleItemTitle = testTextFile2.fileName,
+ moduleItemType = ModuleItemTypes.FILE.stringVal,
+ contentId = testTextFile2.id.toString()
)
- }
- 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, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'. Assert that '${course.name}' course is displayed on the Dashboard.")
+ tokenLogin(teacher)
+ dashboardPage.waitForRender()
- 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, "Open '${course.name}' course and navigate to Modules Page.")
+ dashboardPage.openCourse(course.name)
+ courseBrowserPage.openModulesTab()
- 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, "Assert that both, the '${testTextFile.fileName}' and '${testTextFile2.fileName}' files are published.")
+ moduleListPage.assertModuleItemIsPublished(testTextFile.fileName)
+ moduleListPage.assertModuleItemIsPublished(testTextFile2.fileName)
+
+ Log.d(STEP_TAG, "Click on the 'more menu' of the '${testTextFile.fileName}' file.")
+ moduleListPage.clickItemOverflow(testTextFile.fileName)
+
+ Log.d(STEP_TAG, "Assert that by default, on the Update File Permissions Page the 'Published' radio button is checked within the 'Availability' section.")
+ updateFilePermissionsPage.assertFilePublished()
+
+ Log.d(STEP_TAG, "Assert that the 'Visibility' section radio buttons are enabled and the 'Inherit from course' radio button is checked by default within the 'Visibility' section.")
+ updateFilePermissionsPage.assertVisibilityEnabled()
+ updateFilePermissionsPage.assertFileVisibilityInherit()
+
+ Log.d(STEP_TAG, "Click on the 'Unpublish' radio button and assert that the 'Visibility' section radio buttons became disabled.")
+ updateFilePermissionsPage.clickUnpublishRadioButton()
+ updateFilePermissionsPage.assertVisibilityDisabled()
+
+ Log.d(STEP_TAG, "Click on the 'Update' button and assert on the Module List Page that the '${testTextFile.fileName}' file became unpublished.")
+ updateFilePermissionsPage.clickUpdateButton()
+ moduleListPage.assertModuleItemNotPublished(testTextFile.fileName)
+
+ Log.d(STEP_TAG, "Click on the 'more menu' of the '${testTextFile.fileName}' file.")
+ moduleListPage.clickItemOverflow(testTextFile.fileName)
+
+ Log.d(STEP_TAG, "Assert that the 'Unpublished' radio button is checked because of the previous modifications.")
+ updateFilePermissionsPage.assertFileUnpublished()
+
+ Log.d(STEP_TAG, "Click on the 'Only available with link' (aka. 'Hide') radio button and click on the 'Update' button to save the changes.")
+ updateFilePermissionsPage.clickHideRadioButton()
+ updateFilePermissionsPage.clickUpdateButton()
+
+ Log.d(STEP_TAG, "Assert that the '${testTextFile.fileName}' file module item became hidden.")
+ moduleListPage.assertModuleItemHidden(testTextFile.fileName)
- private fun createDiscussion(
- course: CourseApiModel,
- teacher: CanvasUserApiModel
- ) = DiscussionTopicsApi.createDiscussion(
- courseId = course.id,
- token = teacher.token
- )
+ Log.d(STEP_TAG, "Click on the 'more menu' of the '${testTextFile.fileName}' file.")
+ moduleListPage.clickItemOverflow(testTextFile.fileName)
+
+ Log.d(STEP_TAG, "Assert that the 'Hidden' radio button is checked because of the previous modifications.")
+ updateFilePermissionsPage.assertFileHidden()
+
+ Log.d(STEP_TAG, "Click on the 'Schedule availability' (aka. 'Scheduled') radio button without setting any 'From' and 'Until' dates and click on the 'Update' button to save the changes.")
+ updateFilePermissionsPage.clickScheduleRadioButton()
+ updateFilePermissionsPage.clickUpdateButton()
+
+ Log.d(STEP_TAG, "Assert that the '${testTextFile.fileName}' file is published since that is the expected behaviour if we does not select any dates for schedule.")
+ moduleListPage.assertModuleItemIsPublished(testTextFile.fileName)
+
+ Log.d(STEP_TAG, "Click on the 'more menu' of the '${testTextFile.fileName}' file.")
+ moduleListPage.clickItemOverflow(testTextFile.fileName)
+
+ Log.d(STEP_TAG, "Assert that by default, on the Update File Permissions Page the 'Published' radio button is checked within the 'Availability' section.")
+ updateFilePermissionsPage.assertFilePublished()
+
+ Log.d(STEP_TAG, "Click on the 'Schedule availability' (aka. 'Scheduled') radio button, set some dates and click on the 'Update' button to save the changes.")
+ updateFilePermissionsPage.clickScheduleRadioButton()
+
+ Log.d(PREPARATION_TAG, "Create a calendar with 3 days ago and another one with 3 days later.")
+ val unlockDateCalendar = getCustomDateCalendar(-3)
+ val lockDateCalendar = getCustomDateCalendar(3)
+
+ Log.d(STEP_TAG, "Set the 'From' and 'Until' dates (and times) as well. These are coming from the calendars which has been previously created.")
+ updateFilePermissionsPage.setFromDateTime(unlockDateCalendar)
+ updateFilePermissionsPage.setUntilDateTime(lockDateCalendar)
+
+ Log.d(STEP_TAG, "Click on the 'Update' button to save the changes.")
+ updateFilePermissionsPage.clickUpdateButton()
+
+ Log.d(STEP_TAG, "Assert that the '${testTextFile.fileName}' file module item is scheduled.")
+ moduleListPage.assertModuleItemScheduled(testTextFile.fileName)
+ }
}
\ 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 874ddd528b..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
@@ -4,14 +4,11 @@ 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
+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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
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 = "
Unpublished Page Text
")
+ 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 = "
Unpublished Page Text
")
- Log.d(PREPARATION_TAG,"Create a published page for course: ${course.name}.")
- val testPage2 = createCoursePage(course, teacher, published = true, frontPage = false, body = "
Regular Page Text
")
+ 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 = "
Regular Page Text
")
- Log.d(PREPARATION_TAG,"Create a front page for course: ${course.name}.")
- val testPage3 = createCoursePage(course, teacher, published = true, frontPage = true, body = "
Front Page Text
")
+ 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 = "
Front Page Text
")
- 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 5baed3c839..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
@@ -19,19 +19,17 @@ package com.instructure.teacher.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.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
import com.instructure.dataseeding.util.iso8601
import com.instructure.espresso.ViewUtils
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.teacher.ui.pages.PeopleListPage
import com.instructure.teacher.ui.pages.PersonContextPage
import com.instructure.teacher.ui.utils.TeacherTest
@@ -41,9 +39,11 @@ import com.instructure.teacher.ui.utils.seedData
import com.instructure.teacher.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+import java.lang.Thread.sleep
@HiltAndroidTest
class PeopleE2ETest: TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -61,7 +61,19 @@ class PeopleE2ETest: TeacherTest() {
val course = data.coursesList[0]
val parent = data.parentsList[0]
- Log.d(PREPARATION_TAG,"Seed a 'Text Entry' assignment for course: ${course.name}.")
+ Log.d(PREPARATION_TAG,"Seed some group info.")
+ val groupCategory = GroupsApi.createCourseGroupCategory(data.coursesList[0].id, teacher.token)
+ val groupCategory2 = GroupsApi.createCourseGroupCategory(data.coursesList[0].id, teacher.token)
+ 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.")
+ GroupsApi.createGroupMembership(group.id, gradedStudent.id, teacher.token)
+
+ 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}'.")
val assignments = seedAssignments(
courseId = course.id,
dueAt = 1.days.fromNow.iso8601,
@@ -70,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,
@@ -81,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()
@@ -104,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)
@@ -114,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()
@@ -131,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()
@@ -146,12 +158,52 @@ class PeopleE2ETest: TeacherTest() {
peopleListPage.assertSearchResultCount(1)
peopleListPage.assertPersonListed(gradedStudent)
- Log.d(STEP_TAG, "Click on 'Reset' search (X) icon and assert that all the poeple are displayed (5).")
+ Log.d(STEP_TAG, "Click on 'Reset' search (X) icon and assert that all the people are displayed (5).")
peopleListPage.searchable.clickOnClearSearchButton()
peopleListPage.assertSearchResultCount(5)
+ Log.d(STEP_TAG, "Quit from searching and navigate to People List page.")
+ ViewUtils.pressBackButton(2)
+
+ Log.d(STEP_TAG, "Click on the 'Filter' icon on the top-right corner and select '${group.name}' group as a filter.")
+ peopleListPage.clickOnPeopleFilterMenu()
+ peopleListPage.selectFilter(listOf(group.name))
+
+ Log.d(STEP_TAG, "Assert that the filter title is the previously selected, '${group.name}' group.")
+ peopleListPage.assertFilterTitle(group.name)
+
+ Log.d(STEP_TAG, "Assert that only 1 person matches for the filter, and it is '${gradedStudent.name}', the graded student.")
+ peopleListPage.assertSearchResultCount(1)
+ peopleListPage.assertPersonListed(gradedStudent)
+
+ Log.d(STEP_TAG, "Clear the filter and assert that the list title became 'All People' and all the people are displayed again.")
+ peopleListPage.clickOnClearFilter()
+ sleep(1000) //Allow the clear filter process to propagate.
+ peopleListPage.assertFilterTitle("All People")
+ peopleListPage.assertSearchResultCount(5)
+
+ Log.d(STEP_TAG, "Click on the 'Filter' icon on the top-right corner and select '${group.name}' and '${group2.name}' groups as a filters.")
+ peopleListPage.clickOnPeopleFilterMenu()
+ peopleListPage.selectFilter(listOf(group.name, group2.name))
+
+ Log.d(STEP_TAG, "Assert that the filter title is the previously selected TWO groups: '${group.name}' and '${group2.name}'.")
+ //The order of how the filter title is generated is inconsistent, so we check both way if group1 is the leading and if group2.
+ try { peopleListPage.assertFilterTitle(group.name + ", " + group2.name) }
+ catch(e: AssertionError) { peopleListPage.assertFilterTitle(group2.name + ", " + group.name) }
+
+ Log.d(STEP_TAG, "Assert that only that 2 people matches for the filter, and they are '${gradedStudent.name}' and '${notGradedStudent.name}'.")
+ peopleListPage.assertSearchResultCount(2)
+ peopleListPage.assertPersonListed(gradedStudent)
+ peopleListPage.assertPersonListed(notGradedStudent)
+
+ Log.d(STEP_TAG, "Clear the filter and assert that the list title became 'All People' and all the people are displayed again.")
+ peopleListPage.clickOnClearFilter()
+ sleep(1000) //Allow the clear filter process to propagate.
+ peopleListPage.assertFilterTitle("All People")
+ peopleListPage.assertSearchResultCount(5)
+
Log.d(STEP_TAG, "Navigate back to Dashboard Page. Click on the Inbox bottom menu. Assert that the 'All' section is empty.")
- ViewUtils.pressBackButton(4)
+ ViewUtils.pressBackButton(2)
dashboardPage.openInbox()
inboxPage.assertInboxEmpty()
@@ -162,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 11cf2fe334..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
@@ -18,19 +18,25 @@ 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.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
-import com.instructure.teacher.ui.utils.*
+import com.instructure.teacher.ui.utils.TeacherTest
+import com.instructure.teacher.ui.utils.seedData
+import com.instructure.teacher.ui.utils.seedQuizQuestion
+import com.instructure.teacher.ui.utils.seedQuizSubmission
+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 QuizE2ETest: TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -46,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 ff35dc9f9d..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
@@ -20,12 +20,12 @@ import android.util.Log
import androidx.test.espresso.Espresso
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.TestCategory
+import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvasapi2.utils.RemoteConfigParam
import com.instructure.canvasapi2.utils.RemoteConfigUtils
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
import com.instructure.teacher.ui.utils.TeacherTest
import com.instructure.teacher.ui.utils.seedData
import com.instructure.teacher.ui.utils.tokenLogin
@@ -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 175815be85..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
@@ -23,27 +23,30 @@ import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast
import androidx.test.espresso.matcher.ViewMatchers.withText
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.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
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+import com.instructure.espresso.retry
import com.instructure.teacher.R
-import com.instructure.teacher.ui.utils.*
+import com.instructure.teacher.ui.utils.TeacherTest
+import com.instructure.teacher.ui.utils.seedAssignmentSubmission
+import com.instructure.teacher.ui.utils.seedAssignments
+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 SpeedGraderE2ETest : TeacherTest() {
+
override fun displaysPageObjects() = Unit
override fun enableAndConfigureAccessibilityChecks() = Unit
@@ -61,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,
@@ -70,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,
@@ -81,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,
@@ -92,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)
@@ -127,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()
@@ -153,12 +156,14 @@ class SpeedGraderE2ETest : TeacherTest() {
assignmentSubmissionListPage.clickFilterDialogOk()
Log.d(STEP_TAG,"Assert that there is one submission displayed.")
- assignmentSubmissionListPage.assertHasSubmission(1)
+ retry(times = 5, delay = 3000, catchBlock = { refresh() }) {
+ assignmentSubmissionListPage.assertHasSubmission(1)
+ }
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.")
@@ -184,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 f8715d4820..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.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.model.SubmissionType
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
-import com.instructure.teacher.ui.utils.*
+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 f6edb051c0..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.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.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..8b1cd6c598 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,13 @@
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.ViewAlphaAssertion
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 +16,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 +40,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 +53,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 +98,30 @@ 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
+ )
+ }
+
+ /**
+ * Assert module status icon alpha value.
+ *
+ * @param moduleItemName The name of the module item.
+ * @param expectedAlphaValue The expected alpha (float) value.
+ */
+ fun assertModuleStatusIconAlpha(moduleItemName: String, expectedAlphaValue: Float) {
+ onView(withId(R.id.moduleItemStatusIcon) + withParent(withId(R.id.publishActions) + hasSibling(withId(R.id.moduleItemTitle) + withText(moduleItemName)))).check(ViewAlphaAssertion(expectedAlphaValue))
}
/**
@@ -121,7 +138,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/PeopleListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt
index a9458e6012..d0c6f6f8d0 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt
@@ -17,6 +17,7 @@
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
+import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
@@ -25,6 +26,7 @@ import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import com.instructure.dataseeding.model.CanvasUserApiModel
import com.instructure.espresso.Searchable
import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.assertHasText
import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
import com.instructure.espresso.page.onView
@@ -33,8 +35,10 @@ import com.instructure.espresso.page.waitForView
import com.instructure.espresso.page.waitForViewWithText
import com.instructure.espresso.page.withAncestor
import com.instructure.espresso.page.withId
+import com.instructure.espresso.page.withParent
import com.instructure.espresso.page.withText
import com.instructure.teacher.R
+import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matcher
import org.hamcrest.Matchers
@@ -130,6 +134,43 @@ class PeopleListPage(val searchable: Searchable) : BasePage(R.id.peopleListPage)
onView(withId(R.id.userRole) + withText(role.roleName) + hasSibling(withId(R.id.userName) + withText(personName))).assertDisplayed()
}
+ /**
+ * Clicks on the People Filter menu item.
+ *
+ */
+ fun clickOnPeopleFilterMenu() {
+ onView(withId(R.id.peopleFilterMenuItem)).click()
+ }
+
+ /**
+ * (De)Select group(s) or section(s) as a filter(s).
+ *
+ * @param filterTextList A String list with the name(s) of the section(s) or group(s) which will be (de)selected for filter.
+ */
+ fun selectFilter(filterTextList: List) {
+ for(filterText in filterTextList) {
+ onView(allOf(withId(R.id.checkbox), hasSibling(withText(filterText)))).perform(click())
+ }
+ onView(withText(android.R.string.ok) + withId(android.R.id.button1)).click()
+ }
+
+ /**
+ * Clicks on the clear filter button.
+ *
+ */
+ fun clickOnClearFilter() {
+ onView(withId(R.id.clearFilterTextView) + withParent(R.id.filterTitleWrapper)).click()
+ }
+
+ /**
+ * Assert filter title
+ *
+ * @param expectedTitle The expected filter title.
+ */
+ fun assertFilterTitle(expectedTitle: String) {
+ onView(withId(R.id.peopleFilter) + withParent(R.id.filterTitleWrapper)).assertHasText(expectedTitle)
+ }
+
/**
* Enum class representing the user roles.
*/
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..252f377e50
--- /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"), 20000)
+ 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..e3c9f2d060
--- /dev/null
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/UpdateFilePermissionsPage.kt
@@ -0,0 +1,189 @@
+/*
+ * 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 android.widget.DatePicker
+import android.widget.NumberPicker
+import android.widget.TimePicker
+import androidx.databinding.adapters.TextViewBindingAdapter.setText
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.contrib.PickerActions
+import androidx.test.espresso.matcher.ViewMatchers.withClassName
+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.getDateInCanvasFormat
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.onViewWithId
+import com.instructure.espresso.page.onViewWithText
+import com.instructure.espresso.page.waitForViewWithClassName
+import com.instructure.espresso.page.waitForViewWithId
+import com.instructure.espresso.page.withId
+import com.instructure.espresso.replaceText
+import com.instructure.espresso.scrollTo
+import com.instructure.espresso.swipeUp
+import com.instructure.espresso.typeText
+import com.instructure.teacher.R
+import org.hamcrest.Matchers
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+
+class UpdateFilePermissionsPage : BasePage() {
+
+ private val updateButton 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 clickUpdateButton() {
+ updateButton.click()
+ }
+
+ fun clickPublishRadioButton() {
+ waitForViewWithId(R.id.publish).click()
+ }
+
+ fun clickUnpublishRadioButton() {
+ waitForViewWithId(R.id.unpublish).click()
+ }
+
+ fun clickHideRadioButton() {
+ waitForViewWithId(R.id.hide).click()
+ }
+
+ fun clickScheduleRadioButton() {
+ waitForViewWithId(R.id.schedule).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)
+ }
+
+ fun setFromDateTime(calendar: Calendar) {
+ availableFromDate.perform(click())
+ waitForViewWithClassName(Matchers.equalTo(DatePicker::class.java.name)).perform(PickerActions.setDate(calendar.get(Calendar.YEAR),
+ calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DAY_OF_MONTH)))
+ onViewWithId(android.R.id.button1).click()
+ availableFromTime.perform(click())
+ onView(withClassName(Matchers.equalTo(TimePicker::class.java.name)))
+ .perform(PickerActions.setTime(calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE)))
+ onViewWithId(android.R.id.button1).click()
+ }
+
+ fun setUntilDateTime(calendar: Calendar) {
+ availableUntilDate.perform(click())
+ waitForViewWithClassName(Matchers.equalTo(DatePicker::class.java.name)).perform(PickerActions.setDate(calendar.get(Calendar.YEAR),
+ calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DAY_OF_MONTH)))
+ onViewWithId(android.R.id.button1).click()
+ availableUntilTime.perform(click())
+ onView(withClassName(Matchers.equalTo(TimePicker::class.java.name)))
+ .perform(PickerActions.setTime(calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE)))
+ onViewWithId(android.R.id.button1).click()
+ }
+}
\ 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..91337ad3f7 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,19 +276,10 @@ 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 {
+fun TeacherTest.uploadTextFile(courseId: Long, assignmentId: Long? = null, token: String, fileUploadType: FileUploadType): AttachmentApiModel {
// Create the file
val file = File(
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..d801f2b45e 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
@@ -57,6 +57,7 @@ import com.instructure.loginapi.login.dialog.MasqueradingDialog
import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.pandautils.activities.BasePresenterActivity
import com.instructure.pandautils.binding.viewBinding
+import com.instructure.pandautils.dialogs.EditCourseNicknameDialog
import com.instructure.pandautils.dialogs.RatingDialog
import com.instructure.pandautils.features.help.HelpDialogFragment
import com.instructure.pandautils.features.inbox.list.InboxFragment
@@ -73,7 +74,6 @@ import com.instructure.teacher.R
import com.instructure.teacher.databinding.ActivityInitBinding
import com.instructure.teacher.databinding.NavigationDrawerBinding
import com.instructure.teacher.dialog.ColorPickerDialog
-import com.instructure.teacher.dialog.EditCourseNicknameDialog
import com.instructure.teacher.events.CourseUpdatedEvent
import com.instructure.teacher.events.ToDoListUpdatedEvent
import com.instructure.teacher.factory.InitActivityPresenterFactory
@@ -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/activities/SpeedGraderActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/SpeedGraderActivity.kt
index e637051f83..1a2dae9674 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/activities/SpeedGraderActivity.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/SpeedGraderActivity.kt
@@ -57,6 +57,9 @@ import com.instructure.teacher.adapters.SubmissionContentAdapter
import com.instructure.teacher.databinding.ActivitySpeedgraderBinding
import com.instructure.teacher.events.AssignmentGradedEvent
import com.instructure.teacher.factory.SpeedGraderPresenterFactory
+import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListPresenter
+import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionRepository
+import com.instructure.teacher.features.assignment.submission.SubmissionListFilter
import com.instructure.teacher.features.speedgrader.commentlibrary.CommentLibraryAction
import com.instructure.teacher.features.speedgrader.commentlibrary.CommentLibraryFragment
import com.instructure.teacher.features.speedgrader.commentlibrary.CommentLibraryViewModel
@@ -75,12 +78,16 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.*
+import javax.inject.Inject
@PageView("courses/{courseId}/gradebook/speed_grader?assignment_id={assignmentId}")
@ScreenView(SCREEN_VIEW_SPEED_GRADER)
@AndroidEntryPoint
class SpeedGraderActivity : BasePresenterActivity(), SpeedGraderView {
+ @Inject
+ lateinit var repository: AssignmentSubmissionRepository
+
private val binding by viewBinding(ActivitySpeedgraderBinding::inflate)
/* These should be passed to the presenter factory and should not be directly referenced otherwise */
@@ -89,9 +96,15 @@ class SpeedGraderActivity : BasePresenterActivity by lazy { intent.extras!!.getParcelableArrayList(Const.SUBMISSION) ?: arrayListOf() }
private val discussionTopicHeader: DiscussionTopicHeader? by lazy { intent.extras!!.getParcelable(Const.DISCUSSION_HEADER) }
private val anonymousGrading: Boolean? by lazy { intent.extras?.getBoolean(Const.ANONYMOUS_GRADING) }
+ private val filter: SubmissionListFilter by lazy {
+ intent.extras!!.getSerializable(
+ FILTER
+ ) as? SubmissionListFilter
+ ?: SubmissionListFilter.ALL
+ }
+ private val filterValue: Double by lazy { intent.extras!!.getDouble(FILTER_VALUE) }
private val initialSelection: Int by lazy { intent.extras!!.getInt(Const.SELECTED_ITEM, 0) }
private var currentSelection = 0
@@ -124,9 +137,11 @@ class SpeedGraderActivity : BasePresenterActivity, selectedIdx: Int, anonymousGrading: Boolean? = null): Bundle {
return Bundle().apply {
putLong(Const.COURSE_ID, courseId)
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/adapters/AssignmentAdapter.kt b/apps/teacher/src/main/java/com/instructure/teacher/adapters/AssignmentAdapter.kt
index 0d494256f9..173f9534f2 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/adapters/AssignmentAdapter.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/adapters/AssignmentAdapter.kt
@@ -29,7 +29,7 @@ import com.instructure.teacher.databinding.AdapterAssignmentBinding
import com.instructure.teacher.databinding.AdapterAssignmentGroupHeaderBinding
import com.instructure.teacher.holders.AssignmentGroupHeaderViewHolder
import com.instructure.teacher.holders.AssignmentViewHolder
-import com.instructure.teacher.presenters.AssignmentListPresenter
+import com.instructure.teacher.features.assignment.list.AssignmentListPresenter
import com.instructure.teacher.viewinterface.AssignmentListView
import instructure.androidblueprint.SyncExpandableRecyclerAdapter
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/adapters/GradeableStudentSubmissionAdapter.kt b/apps/teacher/src/main/java/com/instructure/teacher/adapters/GradeableStudentSubmissionAdapter.kt
index cfc13e4e9c..4b769b4225 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/adapters/GradeableStudentSubmissionAdapter.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/adapters/GradeableStudentSubmissionAdapter.kt
@@ -24,7 +24,7 @@ import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.GradeableStudentSubmission
import com.instructure.teacher.databinding.AdapterGradeableStudentSubmissionBinding
import com.instructure.teacher.holders.GradeableStudentSubmissionViewHolder
-import com.instructure.teacher.presenters.AssignmentSubmissionListPresenter
+import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListPresenter
import com.instructure.teacher.viewinterface.AssignmentSubmissionListView
import instructure.androidblueprint.SyncRecyclerAdapter
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/ApplicationModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/ApplicationModule.kt
index c4cd8b0482..dba25af48c 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/di/ApplicationModule.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/di/ApplicationModule.kt
@@ -16,12 +16,13 @@
*/
package com.instructure.teacher.di
+import com.instructure.pandautils.utils.LogoutHelper
+import com.instructure.teacher.utils.TeacherLogoutHelper
import com.instructure.teacher.utils.TeacherPrefs
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
/**
* Module that provides all the application scope dependencies, that are not related to other module.
@@ -34,4 +35,9 @@ class ApplicationModule {
fun provideTeacherPrefs(): TeacherPrefs {
return TeacherPrefs
}
+
+ @Provides
+ fun provideLogoutHelper(): LogoutHelper {
+ return TeacherLogoutHelper()
+ }
}
\ No newline at end of file
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/di/FileDetailsModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/FileDetailsModule.kt
new file mode 100644
index 0000000000..3f5a6dfd22
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/di/FileDetailsModule.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 - 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.teacher.di
+
+import android.webkit.MimeTypeMap
+import com.instructure.canvasapi2.apis.FeaturesAPI
+import com.instructure.canvasapi2.apis.FileFolderAPI
+import com.instructure.teacher.features.files.details.FileDetailsRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class FileDetailsModule {
+ @Provides
+ fun provideFileDetailsRepository(
+ fileFolderApi: FileFolderAPI.FilesFoldersInterface,
+ featuresApi: FeaturesAPI.FeaturesInterface
+ ): FileDetailsRepository {
+ return FileDetailsRepository(fileFolderApi, featuresApi)
+ }
+
+ @Provides
+ fun providesMimeTypeMap(): MimeTypeMap {
+ return MimeTypeMap.getSingleton()
+ }
+}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/ModuleProgressionModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/ModuleProgressionModule.kt
new file mode 100644
index 0000000000..16c3959872
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/di/ModuleProgressionModule.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2022 - 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.teacher.di
+
+import com.instructure.canvasapi2.apis.ModuleAPI
+import com.instructure.teacher.features.modules.progression.ModuleProgressionRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class ModuleProgressionModule {
+ @Provides
+ fun provideModuleProgressionRepository(moduleApi: ModuleAPI.ModuleInterface): ModuleProgressionRepository {
+ return ModuleProgressionRepository(moduleApi)
+ }
+}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/dialog/EditCourseNicknameDialog.kt b/apps/teacher/src/main/java/com/instructure/teacher/dialog/EditCourseNicknameDialog.kt
deleted file mode 100644
index 0c865c89dd..0000000000
--- a/apps/teacher/src/main/java/com/instructure/teacher/dialog/EditCourseNicknameDialog.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-package com.instructure.teacher.dialog
-
-/*
- * Copyright (C) 2017 - 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 .
- *
- */
-import android.app.Dialog
-import android.os.Bundle
-import android.view.View
-import android.view.WindowManager
-import android.view.inputmethod.EditorInfo
-import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatDialog
-import androidx.appcompat.app.AppCompatDialogFragment
-import androidx.appcompat.widget.AppCompatEditText
-import androidx.fragment.app.FragmentManager
-import com.instructure.canvasapi2.models.Course
-import com.instructure.pandautils.analytics.SCREEN_VIEW_EDIT_COURSE_NICKNAME
-import com.instructure.pandautils.analytics.ScreenView
-import com.instructure.pandautils.utils.*
-import com.instructure.teacher.R
-import java.util.Locale
-import kotlin.properties.Delegates
-
-@ScreenView(SCREEN_VIEW_EDIT_COURSE_NICKNAME)
-class EditCourseNicknameDialog : AppCompatDialogFragment() {
-
- private var mEditNicknameCallback: (String) -> Unit by Delegates.notNull()
-
- init {
- retainInstance = true
- }
-
- companion object {
- fun getInstance(manager: FragmentManager, course: Course, callback: (String) -> Unit) : EditCourseNicknameDialog {
- manager.dismissExisting()
- val dialog = EditCourseNicknameDialog()
- val args = Bundle()
- args.putParcelable(Const.COURSE, course)
- dialog.arguments = args
- dialog.mEditNicknameCallback = callback
- return dialog
- }
- }
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val course : Course = nonNullArgs.get(Const.COURSE) as Course
- val view = View.inflate(requireActivity(), R.layout.dialog_course_nickname, null)
- val editCourseNicknameEditText = view.findViewById(R.id.newCourseNickname)
- editCourseNicknameEditText.setText(course.name)
- ViewStyler.themeEditText(requireContext(), editCourseNicknameEditText, ThemePrefs.brandColor)
- editCourseNicknameEditText.inputType = EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
- editCourseNicknameEditText.selectAll()
-
- val nameDialog = AlertDialog.Builder(requireActivity())
- .setCancelable(true)
- .setTitle(getString(R.string.edit_course_nickname))
- .setView(view)
- .setPositiveButton(getString(android.R.string.ok).uppercase(Locale.getDefault())) { _, _ ->
- mEditNicknameCallback(editCourseNicknameEditText.text.toString())
- }
- .setNegativeButton(getString(android.R.string.cancel).uppercase(Locale.getDefault()), null)
- .create()
- nameDialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
-
- nameDialog.setOnShowListener {
- nameDialog.getButton(AppCompatDialog.BUTTON_POSITIVE).setTextColor(ThemePrefs.textButtonColor)
- nameDialog.getButton(AppCompatDialog.BUTTON_NEGATIVE).setTextColor(ThemePrefs.textButtonColor)
- }
- return nameDialog
- }
-
- override fun onDestroyView() {
- // Fix for rotation bug
- dialog?.let { if (retainInstance) it.setDismissMessage(null) }
- super.onDestroyView()
- }
-}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/dialog/LegalDialog.kt b/apps/teacher/src/main/java/com/instructure/teacher/dialog/LegalDialog.kt
index ed86da7f15..92350b18d7 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/dialog/LegalDialog.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/dialog/LegalDialog.kt
@@ -85,7 +85,7 @@ class LegalDialog : AppCompatDialogFragment() {
}
binding.privacyPolicy.setOnClickListener {
- val intent = InternalWebViewActivity.createIntent(requireActivity(), "https://www.instructure.com/canvas/privacy", getString(R.string.privacyPolicy), false)
+ val intent = InternalWebViewActivity.createIntent(requireActivity(), "https://www.instructure.com/policies/product-privacy-policy", getString(R.string.privacyPolicy), false)
requireActivity().startActivity(intent)
dialog.dismiss()
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentDetailPresenterFactory.kt b/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentDetailPresenterFactory.kt
index 41e385eadf..704b4e6a97 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentDetailPresenterFactory.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentDetailPresenterFactory.kt
@@ -17,7 +17,7 @@
package com.instructure.teacher.factory
import com.instructure.canvasapi2.models.Assignment
-import com.instructure.teacher.presenters.AssignmentDetailsPresenter
+import com.instructure.teacher.features.assignment.details.AssignmentDetailsPresenter
import com.instructure.teacher.viewinterface.AssignmentDetailsView
import instructure.androidblueprint.PresenterFactory
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentListPresenterFactory.kt b/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentListPresenterFactory.kt
index 3e14ccb521..314917161c 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentListPresenterFactory.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentListPresenterFactory.kt
@@ -17,7 +17,7 @@
package com.instructure.teacher.factory
import com.instructure.canvasapi2.models.CanvasContext
-import com.instructure.teacher.presenters.AssignmentListPresenter
+import com.instructure.teacher.features.assignment.list.AssignmentListPresenter
import com.instructure.teacher.viewinterface.AssignmentListView
import instructure.androidblueprint.PresenterFactory
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentSubmissionListPresenterFactory.kt b/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentSubmissionListPresenterFactory.kt
index cd10d97f5e..3f700b71bf 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentSubmissionListPresenterFactory.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/factory/AssignmentSubmissionListPresenterFactory.kt
@@ -17,10 +17,17 @@
package com.instructure.teacher.factory
import com.instructure.canvasapi2.models.Assignment
-import com.instructure.teacher.presenters.AssignmentSubmissionListPresenter
+import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListPresenter
+import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionRepository
+import com.instructure.teacher.features.assignment.submission.SubmissionListFilter
import com.instructure.teacher.viewinterface.AssignmentSubmissionListView
import instructure.androidblueprint.PresenterFactory
-class AssignmentSubmissionListPresenterFactory(private var mAssignment: Assignment, private var mFilter: AssignmentSubmissionListPresenter.SubmissionListFilter) : PresenterFactory {
- override fun create(): AssignmentSubmissionListPresenter = AssignmentSubmissionListPresenter(mAssignment, mFilter)
+class AssignmentSubmissionListPresenterFactory(
+ private var assignment: Assignment,
+ private var filter: SubmissionListFilter,
+ private val assignmentSubmissionRepository: AssignmentSubmissionRepository
+) : PresenterFactory {
+ override fun create(): AssignmentSubmissionListPresenter =
+ AssignmentSubmissionListPresenter(assignment, filter, assignmentSubmissionRepository)
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/factory/SpeedGraderPresenterFactory.kt b/apps/teacher/src/main/java/com/instructure/teacher/factory/SpeedGraderPresenterFactory.kt
index b4405bb7fa..258bb126d5 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/factory/SpeedGraderPresenterFactory.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/factory/SpeedGraderPresenterFactory.kt
@@ -17,18 +17,20 @@
package com.instructure.teacher.factory
import com.instructure.canvasapi2.models.DiscussionTopicHeader
-import com.instructure.canvasapi2.models.GradeableStudentSubmission
+import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionRepository
+import com.instructure.teacher.features.assignment.submission.SubmissionListFilter
import com.instructure.teacher.presenters.SpeedGraderPresenter
import com.instructure.teacher.viewinterface.SpeedGraderView
import instructure.androidblueprint.PresenterFactory
-import java.util.*
class SpeedGraderPresenterFactory(
private val courseId: Long,
private val assignmentId: Long,
- private val submissions: ArrayList,
private val submissionId: Long, // Id used when we are coming from a push notification
- private val discussionEntries: DiscussionTopicHeader?
+ private val discussionEntries: DiscussionTopicHeader?,
+ private val repository: AssignmentSubmissionRepository,
+ private val filter: SubmissionListFilter,
+ private val filterValue: Double
) : PresenterFactory {
- override fun create() = SpeedGraderPresenter(courseId, assignmentId, submissions, submissionId, discussionEntries)
+ override fun create() = SpeedGraderPresenter(courseId, assignmentId, submissionId, discussionEntries, repository, filter, filterValue)
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt
similarity index 90%
rename from apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentDetailsFragment.kt
rename to apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt
index dff6ecc5a0..d712f63cea 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentDetailsFragment.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt
@@ -13,9 +13,10 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.instructure.teacher.fragments
+package com.instructure.teacher.features.assignment.details
import android.os.Bundle
+import android.view.LayoutInflater
import android.webkit.WebChromeClient
import android.webkit.WebView
import com.instructure.canvasapi2.models.Assignment
@@ -23,18 +24,34 @@ import com.instructure.canvasapi2.models.Assignment.Companion.getSubmissionTypeF
import com.instructure.canvasapi2.models.Assignment.Companion.submissionTypeToPrettyPrintString
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Course
-import com.instructure.canvasapi2.utils.*
+import com.instructure.canvasapi2.utils.APIHelper
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.canvasapi2.utils.DateHelper
+import com.instructure.canvasapi2.utils.NumberHelper
import com.instructure.canvasapi2.utils.pageview.PageView
import com.instructure.canvasapi2.utils.pageview.PageViewUrl
+import com.instructure.canvasapi2.utils.validOrNull
import com.instructure.interactions.Identity
import com.instructure.interactions.MasterDetailInteractions
import com.instructure.interactions.router.Route
import com.instructure.pandautils.analytics.SCREEN_VIEW_ASSIGNMENT_DETAILS
import com.instructure.pandautils.analytics.ScreenView
-import com.instructure.pandautils.binding.viewBinding
import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment
import com.instructure.pandautils.fragments.BasePresenterFragment
-import com.instructure.pandautils.utils.*
+import com.instructure.pandautils.utils.LongArg
+import com.instructure.pandautils.utils.ParcelableArg
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.pandautils.utils.backgroundColor
+import com.instructure.pandautils.utils.isTablet
+import com.instructure.pandautils.utils.loadHtmlWithIframes
+import com.instructure.pandautils.utils.makeBundle
+import com.instructure.pandautils.utils.onClick
+import com.instructure.pandautils.utils.onClickWithRequireNetwork
+import com.instructure.pandautils.utils.setGone
+import com.instructure.pandautils.utils.setVisible
+import com.instructure.pandautils.utils.toast
+import com.instructure.pandautils.utils.withArgs
import com.instructure.pandautils.views.CanvasWebView
import com.instructure.teacher.R
import com.instructure.teacher.activities.InternalWebViewActivity
@@ -45,8 +62,12 @@ import com.instructure.teacher.events.AssignmentGradedEvent
import com.instructure.teacher.events.AssignmentUpdatedEvent
import com.instructure.teacher.events.post
import com.instructure.teacher.factory.AssignmentDetailPresenterFactory
-import com.instructure.teacher.presenters.AssignmentDetailsPresenter
-import com.instructure.teacher.presenters.AssignmentSubmissionListPresenter
+import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListPresenter
+import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListFragment
+import com.instructure.teacher.features.assignment.submission.SubmissionListFilter
+import com.instructure.teacher.fragments.DueDatesFragment
+import com.instructure.teacher.fragments.EditAssignmentDetailsFragment
+import com.instructure.teacher.fragments.LtiLaunchFragment
import com.instructure.teacher.router.RouteMatcher
import com.instructure.teacher.utils.getColorCompat
import com.instructure.teacher.utils.setupBackButtonWithExpandCollapseAndBack
@@ -57,15 +78,16 @@ import kotlinx.coroutines.Job
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
-import java.util.*
+import java.util.Date
@PageView
@ScreenView(SCREEN_VIEW_ASSIGNMENT_DETAILS)
class AssignmentDetailsFragment : BasePresenterFragment<
AssignmentDetailsPresenter,
- AssignmentDetailsView>(), AssignmentDetailsView, Identity {
-
- private val binding by viewBinding(FragmentAssignmentDetailsBinding::bind)
+ AssignmentDetailsView,
+ FragmentAssignmentDetailsBinding>(),
+ AssignmentDetailsView,
+ Identity {
private var assignment: Assignment by ParcelableArg(Assignment(), ASSIGNMENT)
private var course: Course by ParcelableArg(Course())
@@ -79,7 +101,7 @@ class AssignmentDetailsFragment : BasePresenterFragment<
@PageViewUrl
private fun makePageViewUrl() = "${ApiPrefs.fullDomain}/${course.contextId.replace("_", "s/")}/${assignment.id}"
- override fun layoutResId() = R.layout.fragment_assignment_details
+ override val bindingInflater: (layoutInflater: LayoutInflater) -> FragmentAssignmentDetailsBinding = FragmentAssignmentDetailsBinding::inflate
override fun onRefreshFinished() {}
@@ -372,17 +394,17 @@ class AssignmentDetailsFragment : BasePresenterFragment<
}
submissionsLayout.setOnClickListener {
- navigateToSubmissions(course, assignment, AssignmentSubmissionListPresenter.SubmissionListFilter.ALL)
+ navigateToSubmissions(course, assignment, SubmissionListFilter.ALL)
}
donutGroup.viewAllSubmissions.onClick { submissionsLayout.performClick() } // Separate click listener for a11y
donutGroup.gradedWrapper.setOnClickListener {
- navigateToSubmissions(course, assignment, AssignmentSubmissionListPresenter.SubmissionListFilter.GRADED)
+ navigateToSubmissions(course, assignment, SubmissionListFilter.GRADED)
}
donutGroup.ungradedWrapper.setOnClickListener {
- navigateToSubmissions(course, assignment, AssignmentSubmissionListPresenter.SubmissionListFilter.NOT_GRADED)
+ navigateToSubmissions(course, assignment, SubmissionListFilter.NOT_GRADED)
}
donutGroup.notSubmittedWrapper.setOnClickListener {
- navigateToSubmissions(course, assignment, AssignmentSubmissionListPresenter.SubmissionListFilter.MISSING)
+ navigateToSubmissions(course, assignment, SubmissionListFilter.MISSING)
}
noDescriptionTextView.setOnClickListener { openEditPage(assignment) }
@@ -407,7 +429,7 @@ class AssignmentDetailsFragment : BasePresenterFragment<
}
}
- private fun navigateToSubmissions(course: Course, assignment: Assignment, filter: AssignmentSubmissionListPresenter.SubmissionListFilter) {
+ private fun navigateToSubmissions(course: Course, assignment: Assignment, filter: SubmissionListFilter) {
val args = AssignmentSubmissionListFragment.makeBundle(assignment, filter)
RouteMatcher.route(requireActivity(), Route(null, AssignmentSubmissionListFragment::class.java, course, args))
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/AssignmentDetailsPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsPresenter.kt
similarity index 98%
rename from apps/teacher/src/main/java/com/instructure/teacher/presenters/AssignmentDetailsPresenter.kt
rename to apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsPresenter.kt
index 2d06d7e18e..19529ba06b 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/AssignmentDetailsPresenter.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsPresenter.kt
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.instructure.teacher.presenters
+package com.instructure.teacher.features.assignment.details
import com.instructure.canvasapi2.managers.AssignmentManager
import com.instructure.canvasapi2.managers.SubmissionManager
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/list/AssignmentListFragment.kt
similarity index 97%
rename from apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentListFragment.kt
rename to apps/teacher/src/main/java/com/instructure/teacher/features/assignment/list/AssignmentListFragment.kt
index bf438e8fd3..aeef3a42dc 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentListFragment.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/list/AssignmentListFragment.kt
@@ -14,7 +14,7 @@
* along with this program. If not, see .
*/
-package com.instructure.teacher.fragments
+package com.instructure.teacher.features.assignment.list
import android.os.Bundle
import android.view.Gravity
@@ -40,7 +40,9 @@ import com.instructure.teacher.adapters.AssignmentAdapter
import com.instructure.teacher.databinding.FragmentAssignmentListBinding
import com.instructure.teacher.events.AssignmentUpdatedEvent
import com.instructure.teacher.factory.AssignmentListPresenterFactory
-import com.instructure.teacher.presenters.AssignmentListPresenter
+import com.instructure.teacher.features.assignment.details.AssignmentDetailsFragment
+import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListFragment
+import com.instructure.teacher.fragments.QuizDetailsFragment
import com.instructure.teacher.router.RouteMatcher
import com.instructure.teacher.utils.RecyclerViewUtils
import com.instructure.teacher.utils.setHeaderVisibilityListener
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/AssignmentListPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/list/AssignmentListPresenter.kt
similarity index 99%
rename from apps/teacher/src/main/java/com/instructure/teacher/presenters/AssignmentListPresenter.kt
rename to apps/teacher/src/main/java/com/instructure/teacher/features/assignment/list/AssignmentListPresenter.kt
index ab80e5851d..ae5c7adb17 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/AssignmentListPresenter.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/list/AssignmentListPresenter.kt
@@ -15,7 +15,7 @@
*
*/
-package com.instructure.teacher.presenters
+package com.instructure.teacher.features.assignment.list
import com.instructure.canvasapi2.StatusCallback
import com.instructure.canvasapi2.managers.AssignmentManager
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentSubmissionListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionListFragment.kt
similarity index 79%
rename from apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentSubmissionListFragment.kt
rename to apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionListFragment.kt
index c6e78b5daa..675c01d47c 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentSubmissionListFragment.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionListFragment.kt
@@ -14,7 +14,7 @@
* along with this program. If not, see .
*
*/
-package com.instructure.teacher.fragments
+package com.instructure.teacher.features.assignment.submission
import android.os.Bundle
import android.view.MenuItem
@@ -31,7 +31,15 @@ import com.instructure.pandautils.analytics.SCREEN_VIEW_ASSIGNMENT_SUBMISSION_LI
import com.instructure.pandautils.analytics.ScreenView
import com.instructure.pandautils.binding.viewBinding
import com.instructure.pandautils.fragments.BaseSyncFragment
-import com.instructure.pandautils.utils.*
+import com.instructure.pandautils.utils.ParcelableArg
+import com.instructure.pandautils.utils.SerializableArg
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.pandautils.utils.backgroundColor
+import com.instructure.pandautils.utils.isTablet
+import com.instructure.pandautils.utils.setGone
+import com.instructure.pandautils.utils.setVisible
+import com.instructure.pandautils.utils.withArgs
import com.instructure.teacher.R
import com.instructure.teacher.activities.SpeedGraderActivity
import com.instructure.teacher.adapters.GradeableStudentSubmissionAdapter
@@ -44,18 +52,24 @@ import com.instructure.teacher.events.SubmissionCommentsUpdated
import com.instructure.teacher.events.SubmissionFilterChangedEvent
import com.instructure.teacher.factory.AssignmentSubmissionListPresenterFactory
import com.instructure.teacher.features.postpolicies.ui.PostPolicyFragment
+import com.instructure.teacher.fragments.AddMessageFragment
import com.instructure.teacher.holders.GradeableStudentSubmissionViewHolder
-import com.instructure.teacher.presenters.AssignmentSubmissionListPresenter
-import com.instructure.teacher.presenters.AssignmentSubmissionListPresenter.SubmissionListFilter
import com.instructure.teacher.router.RouteMatcher
-import com.instructure.teacher.utils.*
+import com.instructure.teacher.utils.RecyclerViewUtils
+import com.instructure.teacher.utils.setHeaderVisibilityListener
+import com.instructure.teacher.utils.setupBackButtonAsBackPressedOnly
+import com.instructure.teacher.utils.setupMenu
+import com.instructure.teacher.utils.withRequireNetwork
import com.instructure.teacher.view.QuizSubmissionGradedEvent
import com.instructure.teacher.viewinterface.AssignmentSubmissionListView
+import dagger.hilt.android.AndroidEntryPoint
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
+import javax.inject.Inject
@ScreenView(SCREEN_VIEW_ASSIGNMENT_SUBMISSION_LIST)
+@AndroidEntryPoint
class AssignmentSubmissionListFragment : BaseSyncFragment<
GradeableStudentSubmission,
AssignmentSubmissionListPresenter,
@@ -63,17 +77,20 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
GradeableStudentSubmissionViewHolder,
GradeableStudentSubmissionAdapter>(), AssignmentSubmissionListView {
+ @Inject
+ lateinit var assignmentSubmissionRepository: AssignmentSubmissionRepository
+
private val binding by viewBinding(FragmentAssignmentSubmissionListBinding::bind)
- private var mAssignment: Assignment by ParcelableArg(Assignment(), ASSIGNMENT)
- private var mCourse: Course by ParcelableArg(Course())
+ private var assignment: Assignment by ParcelableArg(Assignment(), ASSIGNMENT)
+ private var course: Course by ParcelableArg(Course())
private lateinit var mRecyclerView: RecyclerView
- private var mFilter by SerializableArg(SubmissionListFilter.ALL, FILTER_TYPE)
- private var mCanvasContextsSelected = ArrayList()
+ private var filter by SerializableArg(SubmissionListFilter.ALL, FILTER_TYPE)
+ private var canvasContextsSelected = ArrayList()
- private var mNeedToForceNetwork = false
+ private var needToForceNetwork = false
- private val mSubmissionFilters: Map by lazy {
+ private val submissionFilters: Map by lazy {
sortedMapOf(
Pair(SubmissionListFilter.ALL.ordinal, getString(R.string.all_submissions)),
Pair(SubmissionListFilter.LATE.ordinal, getString(R.string.submitted_late)),
@@ -87,7 +104,7 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
override fun layoutResId(): Int = R.layout.fragment_assignment_submission_list
override val recyclerView: RecyclerView get() = binding.submissionsRecyclerView
- override fun getPresenterFactory() = AssignmentSubmissionListPresenterFactory(mAssignment, mFilter)
+ override fun getPresenterFactory() = AssignmentSubmissionListPresenterFactory(assignment, filter, assignmentSubmissionRepository)
override fun onCreateView(view: View) = Unit
override fun onPresenterPrepared(presenter: AssignmentSubmissionListPresenter) = with(binding) {
mRecyclerView = RecyclerViewUtils.buildRecyclerView(rootView, requireContext(), adapter, presenter, R.id.swipeRefreshLayout,
@@ -111,8 +128,8 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
mRecyclerView.adapter = adapter
}
- presenter.refresh(mNeedToForceNetwork)
- mNeedToForceNetwork = false
+ presenter.refresh(needToForceNetwork)
+ needToForceNetwork = false
updateFilterTitle()
binding.clearFilterTextView.setTextColor(ThemePrefs.textButtonColor)
@@ -129,11 +146,18 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
}
override fun createAdapter(): GradeableStudentSubmissionAdapter {
- return GradeableStudentSubmissionAdapter(mAssignment, mCourse.id, requireContext(), presenter) { gradeableStudentSubmission ->
+ return GradeableStudentSubmissionAdapter(assignment, course.id, requireContext(), presenter) { gradeableStudentSubmission ->
withRequireNetwork {
val filteredSubmissions = (0 until presenter.data.size()).map { presenter.data[it] }
val selectedIdx = filteredSubmissions.indexOf(gradeableStudentSubmission)
- val bundle = SpeedGraderActivity.makeBundle(mCourse.id, mAssignment.id, filteredSubmissions, selectedIdx, mAssignment.anonymousGrading)
+ val bundle = SpeedGraderActivity.makeBundle(
+ course.id,
+ assignment.id,
+ selectedIdx,
+ assignment.anonymousGrading,
+ presenter.getFilter(),
+ presenter.getFilterPoints()
+ )
RouteMatcher.route(requireActivity(), Route(bundle, RouteContext.SPEED_GRADER))
}
}
@@ -151,7 +175,7 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
swipeRefreshLayout.isRefreshing = false
// Theme the toolbar again since visibilities may have changed
- ViewStyler.themeToolbarColored(requireActivity(), assignmentSubmissionListToolbar, mCourse.backgroundColor, requireContext().getColor(R.color.white))
+ ViewStyler.themeToolbarColored(requireActivity(), assignmentSubmissionListToolbar, course.backgroundColor, requireContext().getColor(R.color.white))
updateStatuses() // Muted is now also set by not being in the new gradebook
}
@@ -170,13 +194,13 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
assignmentSubmissionListToolbar.setupBackButtonAsBackPressedOnly(this@AssignmentSubmissionListFragment)
if(isTablet) {
- assignmentSubmissionListToolbar.title = mAssignment.name
+ assignmentSubmissionListToolbar.title = assignment.name
} else {
assignmentSubmissionListToolbar.setNavigationIcon(R.drawable.ic_back_arrow)
assignmentSubmissionListToolbar.title = getString(R.string.submissions)
- assignmentSubmissionListToolbar.subtitle = mCourse.name
+ assignmentSubmissionListToolbar.subtitle = course.name
}
- ViewStyler.themeToolbarColored(requireActivity(), assignmentSubmissionListToolbar, mCourse.backgroundColor, requireContext().getColor(R.color.white))
+ ViewStyler.themeToolbarColored(requireActivity(), assignmentSubmissionListToolbar, course.backgroundColor, requireContext().getColor(R.color.white))
ViewStyler.themeFAB(addMessage)
}
@@ -189,7 +213,12 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
}
addMessage.setOnClickListener {
- val args = AddMessageFragment.createBundle(presenter.getRecipients(), filterTitle.text.toString() + " " + getString(R.string.on) + " " + mAssignment.name, mCourse.contextId, false)
+ val args = AddMessageFragment.createBundle(
+ presenter.getRecipients(),
+ filterTitle.text.toString() + " " + getString(R.string.on) + " " + assignment.name,
+ course.contextId,
+ false
+ )
RouteMatcher.route(requireActivity(), Route(AddMessageFragment::class.java, null, args))
}
}
@@ -205,7 +234,9 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
when (presenter.getFilter()) {
SubmissionListFilter.ALL -> {
filterTitle.setText(R.string.all_submissions)
- clearFilterTextView.setGone()
+ if (presenter.getSectionFilterText().isEmpty()) {
+ clearFilterTextView.setGone()
+ }
}
SubmissionListFilter.LATE -> filterTitle.setText(R.string.submitted_late)
SubmissionListFilter.MISSING -> filterTitle.setText(R.string.havent_submitted_yet)
@@ -230,15 +261,12 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
private fun setFilter(filterIndex: Int = -1, canvasContexts: ArrayList? = null) = with(binding) {
canvasContexts?.let {
- mCanvasContextsSelected = ArrayList()
- mCanvasContextsSelected.addAll(canvasContexts)
+ canvasContextsSelected = ArrayList()
+ canvasContextsSelected.addAll(canvasContexts)
presenter.setSections(canvasContexts)
updateFilterTitle()
-
- filterTitle.text = filterTitle.text.toString().plus(presenter.getSectionFilterText())
- clearFilterTextView.setVisible()
return
}
@@ -260,13 +288,13 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
updateFilterTitle()
}
SubmissionListFilter.BELOW_VALUE.ordinal -> {
- FilterSubmissionByPointsDialog.getInstance(requireFragmentManager(), getString(R.string.scored_less_than), mAssignment.pointsPossible) { points ->
+ FilterSubmissionByPointsDialog.getInstance(requireFragmentManager(), getString(R.string.scored_less_than), assignment.pointsPossible) { points ->
presenter.setFilter(SubmissionListFilter.BELOW_VALUE, points)
updateFilterTitle()
}.show(requireActivity().supportFragmentManager, FilterSubmissionByPointsDialog::class.java.simpleName)
}
SubmissionListFilter.ABOVE_VALUE.ordinal -> {
- FilterSubmissionByPointsDialog.getInstance(requireFragmentManager(), getString(R.string.scored_more_than), mAssignment.pointsPossible) { points ->
+ FilterSubmissionByPointsDialog.getInstance(requireFragmentManager(), getString(R.string.scored_more_than), assignment.pointsPossible) { points ->
presenter.setFilter(SubmissionListFilter.ABOVE_VALUE, points)
updateFilterTitle()
}.show(requireActivity().supportFragmentManager, FilterSubmissionByPointsDialog::class.java.simpleName)
@@ -277,7 +305,7 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
val menuItemCallback: (MenuItem) -> Unit = { item ->
when (item.itemId) {
R.id.filterSubmissions -> {
- val (keys, values) = mSubmissionFilters.toList().unzip()
+ val (keys, values) = submissionFilters.toList().unzip()
val dialog = RadioButtonDialog.getInstance(requireActivity().supportFragmentManager, getString(R.string.filter_submissions), values as ArrayList, keys.indexOf(presenter.getFilter().ordinal)) { idx ->
EventBus.getDefault().post(SubmissionFilterChangedEvent(keys[idx]))
}
@@ -286,18 +314,18 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
}
R.id.filterBySection -> {
//let the user select the course/group they want to see
- PeopleListFilterDialog.getInstance(requireActivity().supportFragmentManager, presenter.getSectionListIds(), mCourse, false) { canvasContexts ->
+ PeopleListFilterDialog.getInstance(requireActivity().supportFragmentManager, presenter.getSectionListIds(), course, false) { canvasContexts ->
EventBus.getDefault().post(SubmissionFilterChangedEvent(canvasContext = canvasContexts))
}.show(requireActivity().supportFragmentManager, PeopleListFilterDialog::class.java.simpleName)
}
R.id.menuPostPolicies -> {
- RouteMatcher.route(requireActivity(), PostPolicyFragment.makeRoute(mCourse, mAssignment))
+ RouteMatcher.route(requireActivity(), PostPolicyFragment.makeRoute(course, assignment))
}
}
}
private fun updateStatuses() {
- if (presenter.mAssignment.anonymousGrading)
+ if (presenter.assignment.anonymousGrading)
binding.anonGradingStatusView.setVisible().text = getString(R.string.anonymousGradingLabel)
}
@@ -306,7 +334,7 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
fun onAssignmentGraded(event: AssignmentGradedEvent) {
event.once(javaClass.simpleName) {
//force network call on resume
- if(presenter.mAssignment.id == it) mNeedToForceNetwork = true
+ if(presenter.assignment.id == it) needToForceNetwork = true
}
}
@@ -315,7 +343,7 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
fun onQuizGraded(event: QuizSubmissionGradedEvent) {
event.once(javaClass.simpleName) {
// Force network call on resume
- if (presenter.mAssignment.id == it.assignmentId) mNeedToForceNetwork = true
+ if (presenter.assignment.id == it.assignmentId) needToForceNetwork = true
}
}
@@ -323,7 +351,7 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onSubmissionCommentUpdated(event: SubmissionCommentsUpdated) {
event.once(AssignmentSubmissionListFragment::class.java.simpleName) {
- mNeedToForceNetwork = true
+ needToForceNetwork = true
}
}
@@ -338,7 +366,7 @@ class AssignmentSubmissionListFragment : BaseSyncFragment<
@JvmStatic val FILTER_TYPE = "filter_type"
fun newInstance(course: Course, args: Bundle) = AssignmentSubmissionListFragment().withArgs(args).apply {
- mCourse = course
+ this.course = course
}
fun makeBundle(assignment: Assignment): Bundle {
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionListModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionListModule.kt
new file mode 100644
index 0000000000..2fa5886f72
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionListModule.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.teacher.features.assignment.submission
+
+import com.instructure.canvasapi2.apis.AssignmentAPI
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.EnrollmentAPI
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(ActivityComponent::class)
+class AssignmentSubmissionListModule {
+
+ @Provides
+ fun provideAssignmentSubmissionListRepository(
+ assignmentApi: AssignmentAPI.AssignmentInterface,
+ enrollmentApi: EnrollmentAPI.EnrollmentInterface,
+ courseApi: CourseAPI.CoursesInterface
+ ): AssignmentSubmissionRepository {
+ return AssignmentSubmissionRepository(assignmentApi, enrollmentApi, courseApi)
+ }
+}
\ No newline at end of file
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/AssignmentSubmissionListPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionListPresenter.kt
similarity index 61%
rename from apps/teacher/src/main/java/com/instructure/teacher/presenters/AssignmentSubmissionListPresenter.kt
rename to apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionListPresenter.kt
index 3669714b44..02c7eb8879 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/AssignmentSubmissionListPresenter.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionListPresenter.kt
@@ -14,43 +14,41 @@
* along with this program. If not, see .
*
*/
-package com.instructure.teacher.presenters
-
-import com.instructure.canvasapi2.managers.AssignmentManager
-import com.instructure.canvasapi2.managers.CourseManager
-import com.instructure.canvasapi2.managers.EnrollmentManager
-import com.instructure.canvasapi2.models.*
+package com.instructure.teacher.features.assignment.submission
+
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.GradeableStudentSubmission
+import com.instructure.canvasapi2.models.Group
+import com.instructure.canvasapi2.models.GroupAssignee
+import com.instructure.canvasapi2.models.Recipient
+import com.instructure.canvasapi2.models.StudentAssignee
+import com.instructure.canvasapi2.models.Submission
+import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.intersectBy
-import com.instructure.canvasapi2.utils.weave.awaitApi
-import com.instructure.canvasapi2.utils.weave.awaitApis
import com.instructure.canvasapi2.utils.weave.weave
import com.instructure.pandautils.utils.AssignmentUtils2
import com.instructure.teacher.utils.getState
import com.instructure.teacher.viewinterface.AssignmentSubmissionListView
import instructure.androidblueprint.SyncPresenter
import kotlinx.coroutines.Job
-import java.util.*
-
-class AssignmentSubmissionListPresenter(val mAssignment: Assignment, private var mFilter: SubmissionListFilter) : SyncPresenter(GradeableStudentSubmission::class.java) {
-
- enum class SubmissionListFilter {
- ALL,
- LATE,
- MISSING,
- NOT_GRADED,
- GRADED,
- BELOW_VALUE,
- ABOVE_VALUE
- }
+import java.util.Locale
+import java.util.Random
+
+class AssignmentSubmissionListPresenter(
+ val assignment: Assignment,
+ private var filter: SubmissionListFilter,
+ private val assignmentSubmissionRepository: AssignmentSubmissionRepository
+) : SyncPresenter(GradeableStudentSubmission::class.java) {
private var apiCalls: Job? = null
- private var mUnfilteredSubmissions: List = emptyList()
- private var mFilteredSubmissions: List = emptyList()
+ private var unfilteredSubmissions: List = emptyList()
+ private var filteredSubmissions: List = emptyList()
- private var mFilterValue: Double = 0.0
+ private var filterValue: Double = 0.0
- private var mSectionsSelected = ArrayList()
+ private var sectionsSelected = ArrayList()
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
override fun loadData(forceNetwork: Boolean) {
@@ -58,7 +56,7 @@ class AssignmentSubmissionListPresenter(val mAssignment: Assignment, private var
if (apiCalls?.isActive == true) return
// Use existing data if we already have it. Unfiltered submissions should be cleared on refresh
- if (!forceNetwork && mUnfilteredSubmissions.isNotEmpty()) {
+ if (!forceNetwork && unfilteredSubmissions.isNotEmpty()) {
setFilteredData()
return
}
@@ -67,44 +65,21 @@ class AssignmentSubmissionListPresenter(val mAssignment: Assignment, private var
apiCalls = weave {
try {
viewCallback?.onRefreshStarted()
- val (gradeableStudents, enrollments, submissions) = awaitApis, List, List>(
- { AssignmentManager.getAllGradeableStudentsForAssignment(mAssignment.courseId, mAssignment.id, forceNetwork, it) },
- { EnrollmentManager.getAllEnrollmentsForCourse(mAssignment.courseId, null, forceNetwork, it) },
- { AssignmentManager.getAllSubmissionsForAssignment(mAssignment.courseId, mAssignment.id, forceNetwork, it) }
- )
-
- val enrollmentMap = enrollments.associateBy { it.user?.id }
- val students = gradeableStudents.distinctBy { it.id }.map {
- // Students need the enrollment info
- var user = enrollmentMap[it.id]?.user
- // Users can be enrolled in multiple sections, so we need to get all of them
- user = user?.copy(
- enrollments = user.enrollments + enrollments.filter { enrollment -> enrollment.userId == user?.id },
- isFakeStudent = it.isFakeStudent
+ unfilteredSubmissions =
+ assignmentSubmissionRepository.getGradeableStudentSubmissions(
+ assignment,
+ assignment.courseId,
+ forceNetwork
)
- // Need to null out the user object to prevent infinite parcels
- user?.enrollments?.forEach { it.user = null }
- user
- }.filterNotNull()
- mUnfilteredSubmissions = if (mAssignment.groupCategoryId > 0 && !mAssignment.isGradeGroupsIndividually) {
- val groups = awaitApi> { CourseManager.getGroupsForCourse(mAssignment.courseId, it, false) }
- .filter { it.groupCategoryId == mAssignment.groupCategoryId }
- makeGroupSubmissions(students, groups, submissions)
- } else {
- val submissionMap = submissions.associateBy { it.userId }
- students.map {
- GradeableStudentSubmission(StudentAssignee(it), submissionMap[it.id])
- }
- }
-
setFilteredData()
} catch (ignore: Throwable) {
+ ignore.printStackTrace()
}
}
}
fun setSections(sections: ArrayList) {
- mSectionsSelected = sections
+ sectionsSelected = sections
setFilteredData()
}
@@ -117,22 +92,22 @@ class AssignmentSubmissionListPresenter(val mAssignment: Assignment, private var
*/
fun getSectionListIds(): ArrayList {
val contextIds = ArrayList()
- mSectionsSelected.forEach {
+ sectionsSelected.forEach {
contextIds.add(it.id)
}
return contextIds
}
fun getSectionFilterText() : String {
- when (mSectionsSelected.isEmpty()) {
+ when (sectionsSelected.isEmpty()) {
true -> return ""
false -> {
// get the title based on Section selected
val title = StringBuilder()
title.append(", ")
- mSectionsSelected.forEachIndexed { index, canvasContext ->
+ sectionsSelected.forEachIndexed { index, canvasContext ->
title.append(canvasContext.name)
- if ((index + 1) < mSectionsSelected.size) {
+ if ((index + 1) < sectionsSelected.size) {
title.append(", ")
}
}
@@ -142,18 +117,18 @@ class AssignmentSubmissionListPresenter(val mAssignment: Assignment, private var
}
fun clearFilterList() {
- mSectionsSelected.clear()
+ sectionsSelected.clear()
}
private fun setFilteredData() {
- mFilteredSubmissions = mUnfilteredSubmissions.filter {
- when (mFilter) {
+ filteredSubmissions = unfilteredSubmissions.filter {
+ when (filter) {
SubmissionListFilter.ALL -> true
- SubmissionListFilter.LATE -> it.submission?.let { mAssignment.getState(it, true) in listOf(AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE, AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE) } ?: false
- SubmissionListFilter.NOT_GRADED -> it.submission?.let { mAssignment.getState(it, true) in listOf(AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED, AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE) || !it.isGradeMatchesCurrentSubmission } ?: false
- SubmissionListFilter.GRADED -> it.submission?.let { mAssignment.getState(it, true) in listOf(AssignmentUtils2.ASSIGNMENT_STATE_GRADED, AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE, AssignmentUtils2.ASSIGNMENT_STATE_GRADED_MISSING) && it.isGradeMatchesCurrentSubmission} ?: false
- SubmissionListFilter.ABOVE_VALUE -> it.submission?.let { it.isGraded && it.score >= mFilterValue } ?: false
- SubmissionListFilter.BELOW_VALUE -> it.submission?.let { it.isGraded && it.score < mFilterValue } ?: false
+ SubmissionListFilter.LATE -> it.submission?.let { assignment.getState(it, true) in listOf(AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE, AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE) } ?: false
+ SubmissionListFilter.NOT_GRADED -> it.submission?.let { assignment.getState(it, true) in listOf(AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED, AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE) || !it.isGradeMatchesCurrentSubmission } ?: false
+ SubmissionListFilter.GRADED -> it.submission?.let { assignment.getState(it, true) in listOf(AssignmentUtils2.ASSIGNMENT_STATE_GRADED, AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE, AssignmentUtils2.ASSIGNMENT_STATE_GRADED_MISSING) && it.isGradeMatchesCurrentSubmission} ?: false
+ SubmissionListFilter.ABOVE_VALUE -> it.submission?.let { it.isGraded && it.score >= filterValue } ?: false
+ SubmissionListFilter.BELOW_VALUE -> it.submission?.let { it.isGraded && it.score < filterValue } ?: false
// Filtering by ASSIGNMENT_STATE_MISSING here doesn't work because it assumes that the due date has already passed, which isn't necessarily the case when the teacher wants to see
// which students haven't submitted yet
SubmissionListFilter.MISSING -> it.submission?.workflowState == "unsubmitted" || it.submission == null
@@ -161,17 +136,17 @@ class AssignmentSubmissionListPresenter(val mAssignment: Assignment, private var
}
// Shuffle if grading anonymously
- if (mAssignment.anonymousGrading) mFilteredSubmissions = mFilteredSubmissions.shuffled(Random(1234))
+ if (assignment.anonymousGrading) filteredSubmissions = filteredSubmissions.shuffled(Random(1234))
data.clear()
// Filter by section if there is a section filter set
- if (mSectionsSelected.isNotEmpty()) {
+ if (sectionsSelected.isNotEmpty()) {
// get list of ids
- val sectionIds = mSectionsSelected.map { it.id }
+ val sectionIds = sectionsSelected.map { it.id }
- mFilteredSubmissions.forEach { submission ->
+ filteredSubmissions.forEach { submission ->
sectionIds.forEach { section ->
if (submission.assignee is StudentAssignee) {
(submission.assignee as StudentAssignee).student.enrollments.forEach {
@@ -184,7 +159,7 @@ class AssignmentSubmissionListPresenter(val mAssignment: Assignment, private var
}
} else {
// No section filter, add all the submission filtered users
- data.addOrUpdate(mFilteredSubmissions)
+ data.addOrUpdate(filteredSubmissions)
}
viewCallback?.onRefreshFinished()
@@ -199,7 +174,7 @@ class AssignmentSubmissionListPresenter(val mAssignment: Assignment, private var
override fun refresh(forceNetwork: Boolean) {
clearData()
- mUnfilteredSubmissions = emptyList()
+ unfilteredSubmissions = emptyList()
loadData(forceNetwork)
}
@@ -207,17 +182,17 @@ class AssignmentSubmissionListPresenter(val mAssignment: Assignment, private var
//In case this filter hasn't been shown yet, we'll need to show the loader so the user
//doesn't see "no items" view first
viewCallback?.onRefreshStarted()
- mFilter = filter
- mFilterValue = value
+ this.filter = filter
+ filterValue = value
setFilteredData()
}
- fun getFilter() : SubmissionListFilter = mFilter
+ fun getFilter() : SubmissionListFilter = filter
- fun getFilterPoints() : Double = mFilterValue
+ fun getFilterPoints() : Double = filterValue
fun getRecipients() : List {
- return mFilteredSubmissions.map { submission ->
+ return filteredSubmissions.map { submission ->
when(val assignee = submission.assignee) {
is StudentAssignee -> Recipient.from(assignee.student)
is GroupAssignee -> Recipient.from(assignee.group)
@@ -227,7 +202,7 @@ class AssignmentSubmissionListPresenter(val mAssignment: Assignment, private var
override fun compare(item1: GradeableStudentSubmission, item2: GradeableStudentSubmission): Int {
// Turns out we do need to sort them by sortable name, but not when anonymous grading is on
- if (item1.assignee is StudentAssignee && item2.assignee is StudentAssignee && !mAssignment.anonymousGrading) {
+ if (item1.assignee is StudentAssignee && item2.assignee is StudentAssignee && !assignment.anonymousGrading) {
return (item1.assignee as StudentAssignee).student.sortableName?.lowercase(Locale.getDefault())
?.compareTo((item2.assignee as StudentAssignee).student.sortableName?.lowercase(
Locale.getDefault()
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionRepository.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionRepository.kt
new file mode 100644
index 0000000000..656bb7aecc
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionRepository.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.teacher.features.assignment.submission
+
+import com.instructure.canvasapi2.apis.AssignmentAPI
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.GradeableStudentSubmission
+import com.instructure.canvasapi2.models.Group
+import com.instructure.canvasapi2.models.GroupAssignee
+import com.instructure.canvasapi2.models.StudentAssignee
+import com.instructure.canvasapi2.models.Submission
+import com.instructure.canvasapi2.models.User
+import com.instructure.canvasapi2.utils.depaginate
+import com.instructure.canvasapi2.utils.intersectBy
+
+class AssignmentSubmissionRepository(
+ private val assignmentApi: AssignmentAPI.AssignmentInterface,
+ private val enrollmentApi: EnrollmentAPI.EnrollmentInterface,
+ private val courseApi: CourseAPI.CoursesInterface
+) {
+
+ suspend fun getGradeableStudentSubmissions(
+ assignment: Assignment,
+ courseId: Long,
+ forceNetwork: Boolean
+ ): List {
+ val params = RestParams(isForceReadFromNetwork = forceNetwork, usePerPageQueryParam = true)
+ val gradeableStudents = assignmentApi.getFirstPageGradeableStudentsForAssignment(
+ courseId,
+ assignment.id,
+ params
+ ).depaginate {
+ assignmentApi.getNextPageGradeableStudents(it, params)
+ }.dataOrThrow
+
+ val enrollments =
+ enrollmentApi.getFirstPageEnrollmentsForCourse(courseId, null, params).depaginate {
+ enrollmentApi.getNextPage(it, params)
+ }.dataOrThrow
+
+ val submissions =
+ assignmentApi.getFirstPageSubmissionsForAssignment(courseId, assignment.id, params)
+ .depaginate {
+ assignmentApi.getNextPageSubmissions(it, params)
+ }.dataOrThrow
+
+ val enrollmentMap = enrollments.associateBy { it.user?.id }
+ val students = gradeableStudents.distinctBy { it.id }.map {
+ var user = enrollmentMap[it.id]?.user
+ user = user?.copy(
+ enrollments = user.enrollments + enrollments.filter { enrollment -> enrollment.userId == user?.id },
+ isFakeStudent = it.isFakeStudent
+ )
+ user?.enrollments?.forEach { it.user = null }
+ user
+ }.filterNotNull()
+
+ val allSubmissions =
+ if (assignment.groupCategoryId > 0 && !assignment.isGradeGroupsIndividually) {
+ val groups = courseApi.getFirstPageGroups(assignment.courseId, params)
+ .depaginate { courseApi.getNextPageGroups(it, params) }.dataOrThrow
+ .filter { it.groupCategoryId == assignment.groupCategoryId }
+ makeGroupSubmissions(students, groups, submissions)
+ } else {
+ val submissionMap = submissions.associateBy { it.userId }
+ students.map {
+ GradeableStudentSubmission(StudentAssignee(it), submissionMap[it.id])
+ }
+ }
+
+ return allSubmissions
+ }
+
+ private fun makeGroupSubmissions(
+ students: List,
+ groups: List,
+ submissions: List
+ ): List {
+ val userMap = students.associateBy { it.id }
+ val groupAssignees = groups.map { GroupAssignee(it, it.users.map { userMap[it.id] ?: it }) }
+
+ val individualIds =
+ students.map { it.id } - groupAssignees.flatMap { it.students.map { it.id } }
+ val individualAssignees = individualIds
+ .map { userMap[it] }
+ .filterNotNull()
+ .map { StudentAssignee(it) }
+
+ val (groupedSubmissions, individualSubmissions) = submissions.partition {
+ (it.group?.id ?: 0L) != 0L
+ }
+ val studentSubmissionMap = individualSubmissions.associateBy { it.userId }
+ val groupSubmissionMap = groupedSubmissions
+ .groupBy { it.group!!.id }
+ .mapValues {
+ it.value.reduce { acc, submission ->
+ acc.submissionComments =
+ acc.submissionComments.intersectBy(submission.submissionComments) {
+ "${it.authorId}|${it.comment}|${it.createdAt?.time}"
+ }
+ acc
+ }
+ }
+ .toMap()
+
+ val groupSubs =
+ groupAssignees.map { GradeableStudentSubmission(it, groupSubmissionMap[it.group.id]) }
+ val individualSubs = individualAssignees.map {
+ GradeableStudentSubmission(
+ it,
+ studentSubmissionMap[it.student.id]
+ )
+ }
+
+ return groupSubs + individualSubs
+ }
+}
\ No newline at end of file
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFilter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFilter.kt
new file mode 100644
index 0000000000..54c34722c3
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFilter.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.teacher.features.assignment.submission
+
+enum class SubmissionListFilter {
+ ALL,
+ LATE,
+ MISSING,
+ NOT_GRADED,
+ GRADED,
+ BELOW_VALUE,
+ ABOVE_VALUE
+}
\ No newline at end of file
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt
index 8630ff64f3..017718db54 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt
@@ -18,6 +18,7 @@ package com.instructure.teacher.features.discussion
import android.annotation.SuppressLint
import android.graphics.Rect
import android.os.Bundle
+import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.webkit.CookieManager
@@ -39,7 +40,6 @@ import com.instructure.interactions.MasterDetailInteractions
import com.instructure.interactions.router.Route
import com.instructure.pandautils.analytics.SCREEN_VIEW_DISCUSSION_DETAILS
import com.instructure.pandautils.analytics.ScreenView
-import com.instructure.pandautils.binding.viewBinding
import com.instructure.pandautils.dialogs.AttachmentPickerDialog
import com.instructure.pandautils.discussions.DiscussionCaching
import com.instructure.pandautils.discussions.DiscussionEntryHtmlConverter
@@ -57,8 +57,10 @@ import com.instructure.teacher.dialog.NoInternetConnectionDialog
import com.instructure.teacher.events.*
import com.instructure.teacher.events.DiscussionEntryEvent
import com.instructure.teacher.factory.DiscussionsDetailsPresenterFactory
+import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListFragment
import com.instructure.teacher.fragments.*
-import com.instructure.teacher.presenters.AssignmentSubmissionListPresenter
+import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListPresenter
+import com.instructure.teacher.features.assignment.submission.SubmissionListFilter
import com.instructure.teacher.presenters.DiscussionsDetailsPresenter
import com.instructure.teacher.router.RouteMatcher
import com.instructure.teacher.utils.*
@@ -74,9 +76,8 @@ import java.util.*
@ScreenView(SCREEN_VIEW_DISCUSSION_DETAILS)
class DiscussionsDetailsFragment : BasePresenterFragment<
DiscussionsDetailsPresenter,
- DiscussionsDetailsView>(), DiscussionsDetailsView, Identity {
-
- private val binding by viewBinding(FragmentDiscussionsDetailsBinding::bind)
+ DiscussionsDetailsView,
+ FragmentDiscussionsDetailsBinding>(), DiscussionsDetailsView, Identity {
//region Member Variables
private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT)
@@ -100,7 +101,7 @@ class DiscussionsDetailsFragment : BasePresenterFragment<
@PageViewUrlParam("topicId")
private fun getTopicId() = discussionTopicHeader.id
- override fun layoutResId(): Int = R.layout.fragment_discussions_details
+ override val bindingInflater: (layoutInflater: LayoutInflater) -> FragmentDiscussionsDetailsBinding = FragmentDiscussionsDetailsBinding::inflate
override fun onRefreshFinished() {
binding.discussionProgressBar.setGone()
@@ -378,21 +379,21 @@ class DiscussionsDetailsFragment : BasePresenterFragment<
RouteMatcher.route(requireActivity(), Route(null, DueDatesFragment::class.java, canvasContext, args))
}
submissionsLayout.setOnClickListener {
- navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, AssignmentSubmissionListPresenter.SubmissionListFilter.ALL)
+ navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, SubmissionListFilter.ALL)
}
binding.donutGroup.viewAllSubmissions.onClick { submissionsLayout.performClick() } // Separate click listener for a11y
binding.donutGroup.gradedWrapper.setOnClickListener {
- navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, AssignmentSubmissionListPresenter.SubmissionListFilter.GRADED)
+ navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, SubmissionListFilter.GRADED)
}
binding.donutGroup.ungradedWrapper.setOnClickListener {
- navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, AssignmentSubmissionListPresenter.SubmissionListFilter.NOT_GRADED)
+ navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, SubmissionListFilter.NOT_GRADED)
}
binding.donutGroup.notSubmittedWrapper.setOnClickListener {
- navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, AssignmentSubmissionListPresenter.SubmissionListFilter.MISSING)
+ navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, SubmissionListFilter.MISSING)
}
}
- private fun navigateToSubmissions(context: CanvasContext, assignment: Assignment, filter: AssignmentSubmissionListPresenter.SubmissionListFilter) {
+ private fun navigateToSubmissions(context: CanvasContext, assignment: Assignment, filter: SubmissionListFilter) {
val args = AssignmentSubmissionListFragment.makeBundle(assignment, filter)
RouteMatcher.route(requireActivity(), Route(null, AssignmentSubmissionListFragment::class.java, context, args))
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsFragment.kt
new file mode 100644
index 0000000000..31a81efa53
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsFragment.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 - 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.teacher.features.files.details
+
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.utils.tryOrNull
+import com.instructure.pandautils.binding.viewBinding
+import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.backgroundColor
+import com.instructure.pandautils.utils.makeBundle
+import com.instructure.pandautils.utils.withArgs
+import com.instructure.teacher.R
+import com.instructure.teacher.databinding.FragmentFileDetailsBinding
+import com.instructure.teacher.fragments.ViewHtmlFragment
+import com.instructure.teacher.fragments.ViewImageFragment
+import com.instructure.teacher.fragments.ViewMediaFragment
+import com.instructure.teacher.fragments.ViewPdfFragment
+import com.instructure.teacher.fragments.ViewUnsupportedFileFragment
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class FileDetailsFragment : Fragment() {
+
+ private val viewModel: FileDetailsViewModel by viewModels()
+ private val binding by viewBinding(FragmentFileDetailsBinding::bind)
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ return inflater.inflate(R.layout.fragment_file_details, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.lifecycleOwner = this
+ binding.viewModel = viewModel
+
+ viewModel.data.observe(viewLifecycleOwner) {
+ setupFragment(getFragment(it.fileData))
+ }
+ }
+
+ private fun setupFragment(fragment: Fragment) {
+ val fragmentTransaction = childFragmentManager.beginTransaction()
+ fragmentTransaction.add(R.id.fragment_container, fragment, fragment::class.java.name)
+ fragmentTransaction.commitAllowingStateLoss()
+ }
+
+ private fun getFragment(fileData: FileViewData): Fragment {
+ val toolbarColor = viewModel.canvasContext.backgroundColor
+ return when (fileData) {
+ is FileViewData.Pdf -> ViewPdfFragment.newInstance(
+ fileData.url,
+ toolbarColor,
+ fileData.editableFile,
+ true
+ )
+
+ is FileViewData.Media -> ViewMediaFragment.newInstance(
+ Uri.parse(fileData.url),
+ fileData.thumbnailUrl,
+ fileData.contentType,
+ fileData.displayName,
+ true,
+ toolbarColor,
+ fileData.editableFile
+ )
+
+ is FileViewData.Image -> ViewImageFragment.newInstance(
+ fileData.title,
+ Uri.parse(fileData.url),
+ fileData.contentType,
+ true,
+ toolbarColor,
+ fileData.editableFile,
+ true
+ )
+
+ is FileViewData.Html -> ViewHtmlFragment.newInstance(
+ ViewHtmlFragment.makeDownloadBundle(
+ fileData.url,
+ fileData.fileName,
+ toolbarColor,
+ fileData.editableFile,
+ true
+ )
+ )
+
+ is FileViewData.Other -> ViewUnsupportedFileFragment.newInstance(
+ Uri.parse(fileData.url),
+ fileData.fileName,
+ fileData.contentType,
+ tryOrNull { Uri.parse(fileData.thumbnailUrl) },
+ R.drawable.ic_document,
+ toolbarColor,
+ fileData.editableFile,
+ true
+ )
+ }
+ }
+
+ companion object {
+ fun makeBundle(canvasContext: CanvasContext, fileUrl: String): Bundle {
+ return canvasContext.makeBundle { putString(Const.FILE_URL, fileUrl) }
+ }
+
+ fun newInstance(bundle: Bundle) = FileDetailsFragment().withArgs(bundle)
+ }
+}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsRepository.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsRepository.kt
new file mode 100644
index 0000000000..5ab3f5a90e
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsRepository.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 - 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.teacher.features.files.details
+
+import com.instructure.canvasapi2.apis.FeaturesAPI
+import com.instructure.canvasapi2.apis.FileFolderAPI
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.FileFolder
+import com.instructure.canvasapi2.models.License
+
+class FileDetailsRepository(
+ private val fileFolderApi: FileFolderAPI.FilesFoldersInterface,
+ private val featuresApi: FeaturesAPI.FeaturesInterface
+) {
+ suspend fun getFileFolderFromURL(url: String): FileFolder {
+ val restParams = RestParams()
+ return fileFolderApi.getFileFolderFromURL(url, restParams).dataOrThrow
+ }
+
+ suspend fun getCourseFeatures(courseId: Long): List {
+ val params = RestParams()
+ return featuresApi.getEnabledFeaturesForCourse(courseId, params).dataOrThrow
+ }
+
+ suspend fun getCourseFileLicences(courseId: Long): List {
+ val params = RestParams()
+ return fileFolderApi.getCourseFileLicenses(courseId, params).dataOrThrow
+ }
+}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsViewData.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsViewData.kt
new file mode 100644
index 0000000000..10ea84efee
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsViewData.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 - 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.teacher.features.files.details
+
+import com.instructure.pandautils.models.EditableFile
+
+data class FileDetailsViewData(
+ val fileData: FileViewData
+)
+
+sealed class FileViewData {
+ data class Pdf(
+ val url: String,
+ val editableFile: EditableFile
+ ) : FileViewData()
+
+ data class Media(
+ val url: String,
+ val thumbnailUrl: String,
+ val contentType: String,
+ val displayName: String,
+ val editableFile: EditableFile
+ ) : FileViewData()
+
+ data class Image(
+ val title: String,
+ val url: String,
+ val contentType: String,
+ val editableFile: EditableFile
+ ) : FileViewData()
+
+ data class Html(
+ val url: String,
+ val fileName: String,
+ val editableFile: EditableFile
+ ) : FileViewData()
+
+ data class Other(
+ val url: String,
+ val fileName: String,
+ val contentType: String,
+ val thumbnailUrl: String,
+ val editableFile: EditableFile
+ ) : FileViewData()
+}
\ No newline at end of file
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsViewModel.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsViewModel.kt
new file mode 100644
index 0000000000..2f3fdf0cb6
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsViewModel.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2023 - 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.teacher.features.files.details
+
+import android.content.res.Resources
+import android.webkit.MimeTypeMap
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.FileFolder
+import com.instructure.canvasapi2.utils.weave.catch
+import com.instructure.canvasapi2.utils.weave.tryLaunch
+import com.instructure.pandautils.models.EditableFile
+import com.instructure.pandautils.mvvm.ViewState
+import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.backgroundColor
+import com.instructure.teacher.R
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class FileDetailsViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val resources: Resources,
+ private val fileDetailsRepository: FileDetailsRepository,
+ private val mimeTypeMap: MimeTypeMap
+) : ViewModel() {
+
+ val canvasContext = savedStateHandle.get(Const.CANVAS_CONTEXT)!!
+ private val fileUrl = savedStateHandle.get(Const.FILE_URL).orEmpty()
+
+ val state: LiveData
+ get() = _state
+ private val _state = MutableLiveData()
+
+ val data: LiveData
+ get() = _data
+ private val _data = MutableLiveData()
+
+ init {
+ loadData()
+ }
+
+ private fun loadData() {
+ viewModelScope.tryLaunch {
+ _state.postValue(ViewState.Loading)
+
+ val file = fileDetailsRepository.getFileFolderFromURL(fileUrl)
+
+ val features = fileDetailsRepository.getCourseFeatures(canvasContext.id)
+
+ val requiresUsageRights = features.contains("usage_rights_required")
+ val licences = if (requiresUsageRights) {
+ fileDetailsRepository.getCourseFileLicences(canvasContext.id)
+ } else {
+ emptyList()
+ }
+
+ val editableFile = EditableFile(
+ file = file,
+ usageRights = requiresUsageRights,
+ licenses = licences,
+ courseColor = canvasContext.backgroundColor,
+ canvasContext = canvasContext,
+ iconRes = R.drawable.ic_document
+ )
+
+ val extension = file.name.orEmpty().substringAfterLast('.')
+
+ val fileViewData = if (file.contentType == "multipart/form-data") {
+ val type = mimeTypeMap.getMimeTypeFromExtension(extension)
+ getFileViewData(file.copy(contentType = type), extension, editableFile)
+ } else {
+ getFileViewData(file, extension, editableFile)
+ }
+
+ _data.postValue(FileDetailsViewData(fileViewData))
+ _state.postValue(ViewState.Success)
+ } catch {
+ _state.postValue(ViewState.Error(resources.getString(R.string.errorOccurred)))
+ }
+ }
+
+ private fun getFileViewData(file: FileFolder, extension: String, editableFile: EditableFile): FileViewData {
+ val url = file.url.orEmpty()
+ val displayName = file.displayName.orEmpty()
+ val contentType = file.contentType.orEmpty()
+ val thumbnailUrl = file.thumbnailUrl.orEmpty()
+
+ return when {
+ contentType == "application/pdf" -> FileViewData.Pdf(
+ url,
+ editableFile
+ )
+
+ contentType.startsWith("video") || contentType.startsWith("audio") -> FileViewData.Media(
+ url,
+ thumbnailUrl,
+ contentType,
+ displayName,
+ editableFile
+ )
+
+ contentType.startsWith("image") -> FileViewData.Image(
+ displayName,
+ url,
+ contentType,
+ editableFile
+ )
+
+ contentType == "text/html" || extension == "htm" || extension == "html" -> FileViewData.Html(
+ url,
+ displayName,
+ editableFile
+ )
+
+ else -> FileViewData.Other(
+ file.url.orEmpty(),
+ file.displayName.orEmpty(),
+ file.contentType.orEmpty(),
+ file.thumbnailUrl.orEmpty(),
+ editableFile
+ )
+ }
+ }
+}
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 a161ad6679..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,42 +17,84 @@
package com.instructure.teacher.features.modules.list
import com.instructure.canvasapi2.CanvasRestAdapter
-import com.instructure.canvasapi2.managers.FeaturesManager
-import com.instructure.canvasapi2.managers.FileFolderManager
+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.*
-import com.instructure.canvasapi2.utils.*
+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
+import com.instructure.canvasapi2.utils.exhaustive
+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.canvasapi2.utils.weave.awaitOrThrow
+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.Job
import kotlinx.coroutines.launch
import retrofit2.Response
-class ModuleListEffectHandler : EffectHandler() {
-
- private var fileInfoJob: Job? = null
-
+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.LoadFileInfo -> {
- loadFileInfo(effect.item, 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
}
@@ -71,29 +113,6 @@ class ModuleListEffectHandler : EffectHandler()
- }
- view?.routeToFile(canvasContext, file, requiresUsageRights, licenses)
- }
- consumer.accept(ModuleListEvent.ModuleItemLoadStatusChanged(setOf(item.id), false))
- }
- }
-
private fun loadNextPage(canvasContext: CanvasContext, lastPageData: ModuleListPageData, scrollToItemId: Long?) {
launch {
try {
@@ -156,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")
}
@@ -181,5 +202,141 @@ 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 4791337700..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,22 +69,69 @@ sealed class ModuleListEffect {
val moduleItem: ModuleItem,
val canvasContext: CanvasContext
) : ModuleListEffect()
- data class LoadFileInfo(
- val item: 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(
@@ -74,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 fd1dc3fb65..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,7 +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
@@ -51,15 +54,12 @@ class ModuleListUpdate : UpdateInit {
val item = model.modules.flatMap { it.items }.first { it.id == event.moduleItemId }
- return if (item.type == "File") {
- // We need to grab additional file info from Canvas to know how to route
- Next.dispatch(setOf(ModuleListEffect.LoadFileInfo(item, model.course)))
- } else {
- Next.dispatch(setOf(ModuleListEffect.ShowModuleItemDetailView(item, model.course)))
- }
+ return Next.dispatch(setOf(ModuleListEffect.ShowModuleItemDetailView(item, model.course)))
}
+
is ModuleListEvent.PageLoaded -> {
val effects = mutableSetOf()
var newModel = model.copy(
@@ -71,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)
}
@@ -79,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
@@ -93,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(
@@ -111,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()) {
@@ -120,6 +129,7 @@ class ModuleListUpdate : UpdateInit {
val itemGroups = event.items.groupBy { it.moduleId }
val newModel = model.copy(
@@ -134,6 +144,7 @@ class ModuleListUpdate : UpdateInit {
val newModel = model.copy(
modules = model.modules.map { module ->
@@ -142,8 +153,194 @@ 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 2263f2e724..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,34 +18,39 @@ 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.FileFolder
-import com.instructure.canvasapi2.models.License
+import com.instructure.canvasapi2.models.ModuleContentDetails
import com.instructure.canvasapi2.models.ModuleItem
-import com.instructure.canvasapi2.utils.tryOrNull
-import com.instructure.interactions.router.Route
import com.instructure.pandarecycler.PaginatedScrollListener
-import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment
-import com.instructure.pandautils.models.EditableFile
+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.backgroundColor
+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.fragments.*
+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
import com.instructure.teacher.utils.setupBackButton
-import com.instructure.teacher.utils.viewMedia
import com.spotify.mobius.functions.Consumer
class ModuleListView(
inflater: LayoutInflater,
parent: ViewGroup,
val course: CanvasContext
-) : MobiusView(inflater, FragmentModuleListBinding::inflate, parent) {
+) : MobiusView(
+ inflater,
+ FragmentModuleListBinding::inflate,
+ parent
+) {
private var consumer: Consumer? = null
@@ -68,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 {
@@ -76,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 {
@@ -104,72 +217,58 @@ class ModuleListView(
}
fun routeToModuleItem(item: ModuleItem, canvasContext: CanvasContext) {
- val route = when (tryOrNull { ModuleItem.Type.valueOf(item.type!!) }) {
- ModuleItem.Type.Assignment -> {
- val args = AssignmentDetailsFragment.makeBundle(item.contentId)
- Route(null, AssignmentDetailsFragment::class.java, canvasContext, args)
- }
- ModuleItem.Type.Discussion -> {
- DiscussionRouterFragment.makeRoute(canvasContext, item.contentId)
- }
- ModuleItem.Type.Page -> {
- val args = PageDetailsFragment.makeBundle(item.pageUrl!!)
- Route(null, PageDetailsFragment::class.java, canvasContext, args)
- }
- ModuleItem.Type.Quiz -> {
- val args = QuizDetailsFragment.makeBundle(item.contentId)
- Route(null, QuizDetailsFragment::class.java, canvasContext, args)
- }
- ModuleItem.Type.ExternalUrl -> {
- val args = InternalWebViewFragment.makeBundle(
- item.externalUrl.orEmpty(),
- item.title.orEmpty()
- )
- Route(null, InternalWebViewFragment::class.java, canvasContext, args)
- }
- ModuleItem.Type.ExternalTool -> {
- val args = LtiLaunchFragment.makeBundle(
- canvasContext = canvasContext,
- url = item.url.orEmpty(),
- title = item.title.orEmpty(),
- sessionLessLaunch = true
- )
- Route(null, LtiLaunchFragment::class.java, canvasContext, args)
- }
- else -> null
- }
+ val route = ModuleProgressionFragment.makeRoute(canvasContext, item.id)
RouteMatcher.route(activity as FragmentActivity, route)
}
- fun routeToFile(
- canvasContext: CanvasContext,
- file: FileFolder,
- requiresUsageRights: Boolean,
- licenses: List
- ) {
- val editableFile = EditableFile(
- file = file,
- usageRights = requiresUsageRights,
- licenses = licenses,
- courseColor = canvasContext.backgroundColor,
- canvasContext = canvasContext,
- iconRes = R.drawable.ic_document
- )
- viewMedia(
- activity = activity as FragmentActivity,
- filename = file.displayName.orEmpty(),
- contentType = file.contentType.orEmpty(),
- url = file.url,
- thumbnailUrl = file.thumbnailUrl,
- displayName = file.displayName,
- iconRes = R.drawable.ic_document,
- toolbarColor = canvasContext.backgroundColor,
- editableFile = editableFile
- )
- }
-
fun scrollToItem(itemId: Long) {
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