- ${widget._infoText}
+ ${widget.infoText}
diff --git a/apps/flutter_parent/test/router/panda_router_test.dart b/apps/flutter_parent/test/router/panda_router_test.dart
index 56b87aa149..ad6cf2c602 100644
--- a/apps/flutter_parent/test/router/panda_router_test.dart
+++ b/apps/flutter_parent/test/router/panda_router_test.dart
@@ -25,20 +25,19 @@ import 'package:flutter_parent/screens/announcements/announcement_details_screen
import 'package:flutter_parent/screens/assignments/assignment_details_screen.dart';
import 'package:flutter_parent/screens/calendar/calendar_screen.dart';
import 'package:flutter_parent/screens/calendar/calendar_widget/calendar_widget.dart';
-import 'package:flutter_parent/screens/courses/details/course_details_interactor.dart';
import 'package:flutter_parent/screens/courses/details/course_details_screen.dart';
import 'package:flutter_parent/screens/courses/routing_shell/course_routing_shell_screen.dart';
import 'package:flutter_parent/screens/dashboard/dashboard_screen.dart';
import 'package:flutter_parent/screens/domain_search/domain_search_screen.dart';
import 'package:flutter_parent/screens/events/event_details_screen.dart';
import 'package:flutter_parent/screens/help/help_screen.dart';
-import 'package:flutter_parent/screens/settings/legal_screen.dart';
import 'package:flutter_parent/screens/help/terms_of_use_screen.dart';
import 'package:flutter_parent/screens/inbox/conversation_list/conversation_list_screen.dart';
import 'package:flutter_parent/screens/login_landing_screen.dart';
import 'package:flutter_parent/screens/not_a_parent_screen.dart';
import 'package:flutter_parent/screens/pairing/qr_pairing_screen.dart';
import 'package:flutter_parent/screens/qr_login/qr_login_tutorial_screen.dart';
+import 'package:flutter_parent/screens/settings/legal_screen.dart';
import 'package:flutter_parent/screens/settings/settings_screen.dart';
import 'package:flutter_parent/screens/splash/splash_screen.dart';
import 'package:flutter_parent/screens/web_login/web_login_screen.dart';
@@ -55,7 +54,6 @@ import '../utils/accessibility_utils.dart';
import '../utils/canvas_model_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';
final _analytics = MockAnalytics();
@@ -369,6 +367,19 @@ void main() {
expect((widget as CourseRoutingShellScreen).courseId, courseId);
expect((widget).type, CourseShellType.frontPage);
});
+
+ test('submissionWebViewRoute returns SimpleWebViewScreen', () {
+ final url = 'https://test.instructure.com';
+ final title = 'Title';
+ final cookies = {'key': 'value'};
+ final widget = _getWidgetFromRoute(PandaRouter.submissionWebViewRoute(url, title, cookies, false)) as SimpleWebViewScreen;
+
+ expect(widget, isA
());
+ expect(widget.url, url);
+ expect(widget.title, title);
+ expect(widget.initialCookies, cookies);
+ expect(widget.limitWebAccess, false);
+ });
});
group('external url handler', () {
@@ -584,7 +595,7 @@ void main() {
);
verify(_analytics.logMessage('Attempting to route INTERNAL url: $url')).called(1);
- verify(_mockNav.pushRoute(any, PandaRouter.simpleWebViewRoute(url, AppLocalizations().webAccessLimitedMessage)));
+ verify(_mockNav.pushRoute(any, PandaRouter.simpleWebViewRoute(url, AppLocalizations().webAccessLimitedMessage, true)));
});
});
diff --git a/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart b/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart
index ac2f283561..95b5f99721 100644
--- a/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart
+++ b/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart
@@ -33,6 +33,7 @@ import 'package:flutter_parent/screens/inbox/create_conversation/create_conversa
import 'package:flutter_parent/utils/common_widgets/error_panda_widget.dart';
import 'package:flutter_parent/utils/common_widgets/loading_indicator.dart';
import 'package:flutter_parent/utils/common_widgets/web_view/html_description_tile.dart';
+import 'package:flutter_parent/utils/common_widgets/web_view/simple_web_view_screen.dart';
import 'package:flutter_parent/utils/common_widgets/web_view/web_content_interactor.dart';
import 'package:flutter_parent/utils/core_extensions/date_time_extensions.dart';
import 'package:flutter_parent/utils/design/canvas_icons_solid.dart';
@@ -48,7 +49,6 @@ import 'package:permission_handler/permission_handler.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() {
@@ -621,4 +621,45 @@ void main() {
expect(find.text(AppLocalizations().assignmentRemindMeDescription), findsOneWidget);
expect((tester.widget(find.byType(Switch)) as Switch).value, false);
});
+
+ testWidgetsWithAccessibilityChecks('shows Submission & Rubric button', (tester) async {
+ when(interactor.loadAssignmentDetails(any, courseId, assignmentId, studentId)).thenAnswer((_) async => AssignmentDetails(assignment: assignment));
+
+ await tester.pumpWidget(TestApp(
+ AssignmentDetailsScreen(
+ courseId: courseId,
+ assignmentId: assignmentId,
+ ),
+ platformConfig: PlatformConfig(mockApiPrefs: {ApiPrefs.KEY_CURRENT_STUDENT: json.encode(serialize(student))}),
+ ));
+
+ // Pump for a duration since we're delaying webview load for the animation
+ await tester.pumpAndSettle(Duration(seconds: 1));
+
+ expect(find.text(AppLocalizations().submissionAndRubric), findsOneWidget);
+ });
+
+ testWidgetsWithAccessibilityChecks('Submission & Rubric button opens SimpleWebViewScreen', (tester) async {
+ when(interactor.loadAssignmentDetails(any, courseId, assignmentId, studentId)).thenAnswer((_) async => AssignmentDetails(assignment: assignment));
+
+ await tester.pumpWidget(TestApp(
+ AssignmentDetailsScreen(
+ courseId: courseId,
+ assignmentId: assignmentId,
+ ),
+ platformConfig: PlatformConfig(mockApiPrefs: {ApiPrefs.KEY_CURRENT_STUDENT: json.encode(serialize(student))}, initWebview: true),
+ ));
+
+ // Pump for a duration since we're delaying webview load for the animation
+ await tester.pumpAndSettle(Duration(seconds: 1));
+
+ await tester.tap(find.text(AppLocalizations().submissionAndRubric));
+ await tester.pumpAndSettle();
+
+ // Check to make sure we're on the SimpleWebViewScreen screen
+ expect(find.byType(SimpleWebViewScreen), findsOneWidget);
+
+ // Check that we have the correct title
+ expect(find.text(AppLocalizations().submission), findsOneWidget);
+ });
}
diff --git a/apps/flutter_parent/test/utils/widgets/webview/simple_web_view_screen_test.dart b/apps/flutter_parent/test/utils/widgets/webview/simple_web_view_screen_test.dart
index 944e670e5b..211e087f4b 100644
--- a/apps/flutter_parent/test/utils/widgets/webview/simple_web_view_screen_test.dart
+++ b/apps/flutter_parent/test/utils/widgets/webview/simple_web_view_screen_test.dart
@@ -29,7 +29,7 @@ void main() {
final url = 'https://www.google.com';
final title = 'title';
- await tester.pumpWidget(TestApp(SimpleWebViewScreen(url, title), platformConfig: config));
+ await tester.pumpWidget(TestApp(SimpleWebViewScreen(url, title, true), platformConfig: config));
await tester.pump();
expect(find.text(title), findsOneWidget);
@@ -39,7 +39,7 @@ void main() {
final url = 'https://www.google.com';
final title = 'title';
- await tester.pumpWidget(TestApp(SimpleWebViewScreen(url, title), platformConfig: config));
+ await tester.pumpWidget(TestApp(SimpleWebViewScreen(url, title, true), platformConfig: config));
await tester.pump();
expect(find.byType(WebView), findsOneWidget);
@@ -51,7 +51,7 @@ void main() {
final title = 'title';
await TestApp.showWidgetFromTap(tester, (context) {
- return Navigator.of(context).push(MaterialPageRoute(builder: (context) => SimpleWebViewScreen(url, title)));
+ return Navigator.of(context).push(MaterialPageRoute(builder: (context) => SimpleWebViewScreen(url, title, true)));
}, config: config);
expect(find.byType(WebView), findsOneWidget);
diff --git a/apps/parent/build.gradle b/apps/parent/build.gradle
index 715191d39e..6f22fb2773 100644
--- a/apps/parent/build.gradle
+++ b/apps/parent/build.gradle
@@ -152,6 +152,8 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = Versions.KOTLIN_COMPOSE_COMPILER_VERSION
}
+
+ testOptions.animationsDisabled = true
}
dependencies {
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/ManageStudentsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/ManageStudentsScreenTest.kt
new file mode 100644
index 0000000000..bb70b4e921
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/ManageStudentsScreenTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.parentapp.ui.compose
+
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasAnyChild
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.instructure.pandares.R
+import com.instructure.pandautils.utils.ThemedColor
+import com.instructure.parentapp.features.managestudents.ColorPickerDialogUiState
+import com.instructure.parentapp.features.managestudents.ManageStudentsScreen
+import com.instructure.parentapp.features.managestudents.ManageStudentsUiState
+import com.instructure.parentapp.features.managestudents.StudentItemUiState
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+@RunWith(AndroidJUnit4::class)
+class ManageStudentsScreenTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assertEmptyContent() {
+ composeTestRule.setContent {
+ ManageStudentsScreen(
+ uiState = ManageStudentsUiState(
+ isLoading = false,
+ studentListItems = emptyList()
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("You are not observing any students.")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Retry")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ composeTestRule.onNodeWithTag(R.drawable.panda_manage_students.toString())
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun assertErrorContent() {
+ composeTestRule.setContent {
+ ManageStudentsScreen(
+ uiState = ManageStudentsUiState(
+ isLoadError = true,
+ studentListItems = emptyList()
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("There was an error loading your students.")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Retry")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ }
+
+ @Test
+ fun assertStudentListContent() {
+ composeTestRule.setContent {
+ ManageStudentsScreen(
+ uiState = ManageStudentsUiState(
+ studentListItems = listOf(
+ StudentItemUiState(
+ studentId = 1,
+ studentName = "John Doe",
+ studentPronouns = "He/Him",
+ studentColor = ThemedColor(R.color.studentGreen)
+ ),
+ StudentItemUiState(
+ studentId = 2,
+ studentName = "Jane Doe",
+ studentColor = ThemedColor(R.color.studentPink)
+ )
+ )
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+ }
+
+ fun studentItemMatcher(name: String) = hasTestTag("studentListItem") and hasAnyChild(hasText(name))
+ composeTestRule.onNode(studentItemMatcher("John Doe (He/Him)"), true)
+ .assertIsDisplayed()
+ composeTestRule.onNode(studentItemMatcher("Jane Doe"), true)
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun assertColorPickerDialogError() {
+ composeTestRule.setContent {
+ ManageStudentsScreen(
+ uiState = ManageStudentsUiState(
+ studentListItems = listOf(
+ StudentItemUiState(
+ studentId = 1,
+ studentName = "John Doe",
+ studentColor = ThemedColor(R.color.studentGreen)
+ )
+ ),
+ colorPickerDialogUiState = ColorPickerDialogUiState(
+ showColorPickerDialog = true,
+ studentId = 1,
+ initialUserColor = null,
+ userColors = emptyList(),
+ isSavingColorError = true
+ )
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("Select Student Color")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("An error occurred while saving your selection. Please try again.")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Cancel")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ composeTestRule.onNodeWithText("OK")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/NotAParentScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/NotAParentScreenTest.kt
new file mode 100644
index 0000000000..70de846d41
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/NotAParentScreenTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.parentapp.ui.compose
+
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.instructure.parentapp.features.notaparent.NotAParentScreen
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+@RunWith(AndroidJUnit4::class)
+class NotAParentScreenTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assertContent() {
+ composeTestRule.setContent {
+ NotAParentScreen(
+ returnToLoginClick = {},
+ onStudentClick = {},
+ onTeacherClick = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("Not a parent?").assertIsDisplayed()
+ composeTestRule.onNodeWithText("We couldn't find any students associated with your account").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Return to login")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ composeTestRule.onNodeWithText("Are you a student or teacher?")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ composeTestRule.onNodeWithText("STUDENT")
+ .assertIsNotDisplayed()
+ }
+
+ @Test
+ fun assertAppOptions() {
+ composeTestRule.setContent {
+ NotAParentScreen(
+ returnToLoginClick = {},
+ onStudentClick = {},
+ onTeacherClick = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("Are you a student or teacher?").performClick()
+ composeTestRule.onNodeWithText("STUDENT")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ composeTestRule.onNodeWithText("TEACHER")
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsListItemTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsListItemTest.kt
new file mode 100644
index 0000000000..252a86d226
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsListItemTest.kt
@@ -0,0 +1,250 @@
+/*
+ * 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.parentapp.ui.compose.alerts
+
+import android.graphics.Color
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.instructure.canvasapi2.models.AlertType
+import com.instructure.parentapp.features.alerts.list.AlertsItemUiState
+import com.instructure.parentapp.features.alerts.list.AlertsListItem
+import com.instructure.parentapp.utils.hasDrawable
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Date
+import com.instructure.parentapp.R
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+@RunWith(AndroidJUnit4::class)
+class AlertsListItemTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assertAssignmentMissing() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Assignment Missing title",
+ alertType = AlertType.ASSIGNMENT_MISSING,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.waitForIdle()
+
+ composeTestRule.onNodeWithText("Assignment missing").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Assignment Missing title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_warning)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertAssignmentGradeHigh() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Assignment Grade High title",
+ alertType = AlertType.ASSIGNMENT_GRADE_HIGH,
+ date = Date(),
+ observerAlertThreshold = "90%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Assignment Grade Above 90%").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Assignment Grade High title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertAssignmentGradeLow() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Assignment Grade Low title",
+ alertType = AlertType.ASSIGNMENT_GRADE_LOW,
+ date = Date(),
+ observerAlertThreshold = "60%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Assignment Grade Below 60%").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Assignment Grade Low title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_warning)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertCourseGradeHigh() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Course Grade High title",
+ alertType = AlertType.COURSE_GRADE_HIGH,
+ date = Date(),
+ observerAlertThreshold = "90%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Course Grade Above 90%").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Course Grade High title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertCourseGradeLow() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Course Grade Low title",
+ alertType = AlertType.COURSE_GRADE_LOW,
+ date = Date(),
+ observerAlertThreshold = "60%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Course Grade Below 60%").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Course Grade Low title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_warning)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertCourseAnnouncement() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Course Announcement title",
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Course Announcement").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Course Announcement title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertInstitutionAnnouncement() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Institution Announcement title",
+ alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Institution Announcement").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Institution Announcement title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertLockedForUser() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Locked for User title",
+ alertType = AlertType.ASSIGNMENT_MISSING,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = true,
+ unread = true,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Assignment missing").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Locked for User title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_lock_lined)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ }
+
+ @Test
+ fun assertRead() {
+ composeTestRule.setContent {
+ AlertsListItem(alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Institution Announcement title",
+ alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = null
+ ), userColor = Color.BLUE, actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("Institution Announcement").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Institution Announcement title").assertIsDisplayed()
+ composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
+ composeTestRule.onNodeWithTag("unreadIndicator").assertIsNotDisplayed()
+ }
+
+ private fun parseDate(date: Date): String {
+ val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.getDefault())
+ val timeFormat = SimpleDateFormat("h:mm a", Locale.getDefault())
+ return "${dateFormat.format(date)} at ${timeFormat.format(date)}"
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsScreenTest.kt
new file mode 100644
index 0000000000..3f2586ed59
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsScreenTest.kt
@@ -0,0 +1,224 @@
+/*
+ * 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.parentapp.ui.compose.alerts
+
+import android.graphics.Color
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasAnyDescendant
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.instructure.canvasapi2.models.AlertType
+import com.instructure.parentapp.features.alerts.list.AlertsItemUiState
+import com.instructure.parentapp.features.alerts.list.AlertsScreen
+import com.instructure.parentapp.features.alerts.list.AlertsUiState
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.text.SimpleDateFormat
+import java.time.Instant
+import java.util.Date
+import java.util.Locale
+
+@ExperimentalMaterialApi
+@RunWith(AndroidJUnit4::class)
+class AlertsScreenTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assertError() {
+ composeTestRule.setContent {
+ AlertsScreen(uiState = AlertsUiState(
+ alerts = emptyList(),
+ isLoading = false,
+ isError = true,
+ isRefreshing = false,
+ studentColor = Color.BLUE,
+ ), actionHandler = {})
+ }
+
+
+ composeTestRule.onNodeWithText("There was an error loading your student’s alerts.")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Retry").assertHasClickAction().assertIsDisplayed()
+ }
+
+ @Test
+ fun assertLoading() {
+ composeTestRule.setContent {
+ AlertsScreen(uiState = AlertsUiState(
+ alerts = emptyList(),
+ isLoading = true,
+ isError = false,
+ isRefreshing = false,
+ studentColor = Color.BLUE,
+ ), actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithTag("loading").assertIsDisplayed()
+ }
+
+ @Test
+ fun assertEmpty() {
+ composeTestRule.setContent {
+ AlertsScreen(uiState = AlertsUiState(
+ alerts = emptyList(),
+ isLoading = false,
+ isError = false,
+ isRefreshing = false,
+ studentColor = Color.BLUE,
+ ), actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithText("No Alerts").assertIsDisplayed()
+ composeTestRule.onNodeWithText("There's nothing to be notified of yet.").assertIsDisplayed()
+ }
+
+ @Test
+ fun assertRefreshing() {
+ composeTestRule.setContent {
+ AlertsScreen(uiState = AlertsUiState(
+ alerts = emptyList(),
+ isLoading = false,
+ isError = false,
+ isRefreshing = true,
+ studentColor = Color.BLUE,
+ ), actionHandler = {})
+ }
+
+ composeTestRule.onNodeWithTag("pullRefreshIndicator").assertIsDisplayed()
+ }
+
+ @Test
+ fun assertContent() {
+ val items = listOf(
+ AlertsItemUiState(
+ alertId = 1,
+ title = "Alert 1",
+ alertType = AlertType.ASSIGNMENT_GRADE_LOW,
+ date = Date.from(Instant.parse("2023-09-15T09:02:00Z")),
+ observerAlertThreshold = "20%",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 2,
+ title = "Alert 2",
+ alertType = AlertType.ASSIGNMENT_GRADE_HIGH,
+ date = Date.from(Instant.parse("2023-09-16T09:02:00Z")),
+ observerAlertThreshold = "80%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 3,
+ title = "Alert 3",
+ alertType = AlertType.COURSE_GRADE_LOW,
+ date = Date.from(Instant.parse("2023-09-17T09:02:00Z")),
+ observerAlertThreshold = "20%",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 4,
+ title = "Alert 4",
+ alertType = AlertType.COURSE_GRADE_HIGH,
+ date = Date.from(Instant.parse("2023-09-18T09:02:00Z")),
+ observerAlertThreshold = "50%",
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 5,
+ title = "Alert 5",
+ alertType = AlertType.ASSIGNMENT_MISSING,
+ date = Date.from(Instant.parse("2023-09-19T09:02:00Z")),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 6,
+ title = "Alert 6",
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
+ date = Date.from(Instant.parse("2023-09-20T09:02:00Z")),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = null
+ ),
+ AlertsItemUiState(
+ alertId = 7,
+ title = "Alert 7",
+ alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
+ date = Date.from(Instant.parse("2023-09-21T09:02:00Z")),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = null
+ )
+ )
+
+ composeTestRule.setContent {
+ AlertsScreen(uiState = AlertsUiState(
+ alerts = items,
+ isLoading = false,
+ isError = false,
+ isRefreshing = false,
+ studentColor = Color.BLUE,
+ ), actionHandler = {})
+ }
+
+ items.forEach {
+ composeTestRule.onNodeWithText(it.title).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseAlertType(it.alertType, it.observerAlertThreshold)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(parseDate(it.date!!)).assertIsDisplayed()
+ composeTestRule.onNode(hasAnyDescendant(hasText(it.title)).and(hasTestTag("alertItem")), useUnmergedTree = true).assertHasClickAction()
+ }
+ }
+
+ private fun parseDate(date: Date): String {
+ val dateFormat = SimpleDateFormat("MMM d", Locale.getDefault())
+ val timeFormat = SimpleDateFormat("h:mm a", Locale.getDefault())
+ return "${dateFormat.format(date)} at ${timeFormat.format(date)}"
+ }
+
+ private fun parseAlertType(alertType: AlertType, threshold: String?): String {
+ return when(alertType) {
+ AlertType.ASSIGNMENT_GRADE_LOW -> "Assignment Grade Below $threshold"
+ AlertType.ASSIGNMENT_GRADE_HIGH -> "Assignment Grade Above $threshold"
+ AlertType.COURSE_GRADE_LOW -> "Course Grade Below $threshold"
+ AlertType.COURSE_GRADE_HIGH -> "Course Grade Above $threshold"
+ AlertType.ASSIGNMENT_MISSING -> "Assignment missing"
+ AlertType.COURSE_ANNOUNCEMENT -> "Course Announcement"
+ AlertType.INSTITUTION_ANNOUNCEMENT -> "Institution Announcement"
+ else -> "Unknown"
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt
new file mode 100644
index 0000000000..7a2d099067
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addObserverAlert
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.AlertType
+import com.instructure.canvasapi2.models.AlertWorkflowState
+import com.instructure.parentapp.utils.ParentComposeTest
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+import org.junit.Test
+import java.util.Date
+
+@HiltAndroidTest
+class AlertsInteractionTest : ParentComposeTest() {
+
+ @Test
+ fun dismissAlert() {
+ val data = initData()
+ goToAlerts(data)
+
+ val student = data.students.first()
+ val observer = data.parents.first()
+ val course = data.courses.values.first()
+
+ val alert = data.addObserverAlert(
+ observer,
+ student,
+ course,
+ AlertType.ASSIGNMENT_MISSING,
+ AlertWorkflowState.UNREAD,
+ Date(),
+ null,
+ false
+ )
+
+ alertsPage.refresh()
+
+ alertsPage.assertAlertItemDisplayed(alert.title)
+
+ composeTestRule.waitForIdle()
+ alertsPage.dismissAlert(alert.title)
+
+ composeTestRule.waitForIdle()
+ alertsPage.assertSnackbar("Alert dismissed")
+ alertsPage.assertAlertItemNotDisplayed(alert.title)
+ alertsPage.refresh()
+ alertsPage.assertAlertItemNotDisplayed(alert.title)
+ }
+
+ @Test
+ fun undoDismiss() {
+ val data = initData()
+ goToAlerts(data)
+
+ val student = data.students.first()
+ val observer = data.parents.first()
+ val course = data.courses.values.first()
+
+ val alert = data.addObserverAlert(
+ observer,
+ student,
+ course,
+ AlertType.ASSIGNMENT_MISSING,
+ AlertWorkflowState.UNREAD,
+ Date(),
+ null,
+ false
+ )
+
+ alertsPage.refresh()
+
+ alertsPage.assertAlertItemDisplayed(alert.title)
+
+ composeTestRule.waitForIdle()
+ alertsPage.dismissAlert(alert.title)
+
+ composeTestRule.waitForIdle()
+ alertsPage.assertSnackbar("Alert dismissed")
+ alertsPage.clickUndo()
+ alertsPage.assertAlertItemDisplayed(alert.title)
+
+ alertsPage.refresh()
+ alertsPage.assertAlertItemDisplayed(alert.title)
+ }
+
+ @Test
+ fun emptyAlerts() {
+ val data = initData()
+ goToAlerts(data)
+
+ alertsPage.assertEmptyState()
+ }
+
+ @Test
+ fun openAlert() {
+ val data = initData()
+ goToAlerts(data)
+
+ val student = data.students.first()
+ val observer = data.parents.first()
+ val course = data.courses.values.first()
+
+ val alert = data.addObserverAlert(
+ observer,
+ student,
+ course,
+ AlertType.ASSIGNMENT_MISSING,
+ AlertWorkflowState.UNREAD,
+ Date(),
+ null,
+ false
+ )
+
+ alertsPage.refresh()
+
+ composeTestRule.waitForIdle()
+ alertsPage.assertAlertItemDisplayed(alert.title)
+ alertsPage.assertAlertUnread(alert.title)
+ alertsPage.clickOnAlert(alert.title)
+
+ //TODO check that we route to the correct screen when ready
+ }
+
+ private fun initData(): MockCanvas {
+ return MockCanvas.init(studentCount = 1, parentCount = 1, courseCount = 1)
+ }
+
+ private fun goToAlerts(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickAlerts()
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt
new file mode 100644
index 0000000000..264ce4131e
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvas.espresso.waitForMatcherWithSleeps
+import com.instructure.loginapi.login.R
+import com.instructure.parentapp.utils.ParentTest
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+import org.junit.Test
+
+
+@HiltAndroidTest
+class DashboardInteractionTest : ParentTest() {
+
+ @Test
+ fun testObserverData() {
+ val data = initData()
+
+ goToDashboard(data)
+
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.assertObserverData(data.parents.first())
+ }
+
+ @Test
+ fun testChangeStudent() {
+ val data = initData()
+
+ goToDashboard(data)
+
+ val students = data.students.sortedBy { it.sortableName }
+ dashboardPage.assertSelectedStudent(students.first().shortName!!)
+ dashboardPage.openStudentSelector()
+ dashboardPage.selectStudent(data.students.last().shortName!!)
+ dashboardPage.assertSelectedStudent(students.last().shortName!!)
+ }
+
+ @Test
+ fun testLogout() {
+ val data = initData()
+
+ goToDashboard(data)
+
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.tapLogout()
+ dashboardPage.assertLogoutDialog()
+ dashboardPage.tapOk()
+ waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check(
+ ViewAssertions.matches(
+ ViewMatchers.isDisplayed()
+ )
+ )
+ }
+
+ @Test
+ fun testSwitchUsers() {
+ val data = initData()
+
+ goToDashboard(data)
+
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.tapSwitchUsers()
+ waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check(
+ ViewAssertions.matches(
+ ViewMatchers.isDisplayed()
+ )
+ )
+ }
+
+ private fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 2,
+ courseCount = 1
+ )
+ }
+
+ private fun goToDashboard(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+ }
+
+ override fun displaysPageObjects() = Unit
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt
new file mode 100644
index 0000000000..9e5b444aec
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.parentapp.ui.pages.ManageStudentsPage
+import com.instructure.parentapp.utils.ParentComposeTest
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+import org.junit.Test
+
+
+@HiltAndroidTest
+class ManageStudentsInteractionTest : ParentComposeTest() {
+
+ private val manageStudentsPage = ManageStudentsPage(composeTestRule)
+
+ @Test
+ fun testStudentsDisplayed() {
+ val data = initData()
+
+ goToManageStudents(data)
+
+ composeTestRule.waitForIdle()
+ data.students.forEach {
+ manageStudentsPage.assertStudentItemDisplayed(it)
+ }
+ }
+
+ @Test
+ fun testStudentTapped() {
+ val data = initData()
+
+ goToManageStudents(data)
+
+ composeTestRule.waitForIdle()
+ manageStudentsPage.tapStudent(data.students.first().shortName!!)
+ // TODO Assert alert settings when implemented
+ }
+
+ @Test
+ fun testColorPickerDialog() {
+ val data = initData()
+
+ goToManageStudents(data)
+
+ composeTestRule.waitForIdle()
+ manageStudentsPage.tapStudentColor(data.students.first().shortName!!)
+ manageStudentsPage.assertColorPickerDialogDisplayed()
+ }
+
+ private fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 3,
+ courseCount = 1
+ )
+ }
+
+ private fun goToManageStudents(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.tapManageStudents()
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt
new file mode 100644
index 0000000000..0bccedad38
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.parentapp.ui.interaction
+
+import android.content.Intent
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.matcher.IntentMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addCourse
+import com.instructure.canvas.espresso.mockCanvas.addEnrollment
+import com.instructure.canvas.espresso.mockCanvas.addUser
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvas.espresso.mockCanvas.updateUserEnrollments
+import com.instructure.canvas.espresso.waitForMatcherWithSleeps
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.loginapi.login.R
+import com.instructure.parentapp.ui.pages.NotAParentPage
+import com.instructure.parentapp.utils.ParentComposeTest
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.CoreMatchers
+import org.junit.Test
+
+
+@HiltAndroidTest
+class NotAParentInteractionsTest : ParentComposeTest() {
+
+ private val notAParentPage = NotAParentPage(composeTestRule)
+
+ @Test
+ fun testLogout() {
+ val data = initData()
+ goToNotAParentScreen(data)
+
+ notAParentPage.tapReturnToLogin()
+ waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check(
+ ViewAssertions.matches(
+ ViewMatchers.isDisplayed()
+ )
+ )
+ }
+
+ @Test
+ fun testTapStudent() {
+ val data = initData()
+ goToNotAParentScreen(data)
+
+ notAParentPage.expandAppOptions()
+ Intents.init()
+ try {
+ val expectedIntent = CoreMatchers.allOf(
+ IntentMatchers.hasAction(Intent.ACTION_VIEW),
+ CoreMatchers.anyOf(
+ // Could be either of these, depending on whether the play store app is installed
+ IntentMatchers.hasData("market://details?id=com.instructure.candroid"),
+ IntentMatchers.hasData("https://play.google.com/store/apps/details?id=com.instructure.candroid")
+ )
+ )
+ notAParentPage.tapApp("STUDENT")
+ Intents.intended(expectedIntent)
+ } finally {
+ Intents.release()
+ }
+ }
+
+ @Test
+ fun testTapTeacher() {
+ val data = initData()
+ goToNotAParentScreen(data)
+
+ notAParentPage.expandAppOptions()
+ Intents.init()
+ try {
+ val expectedIntent = CoreMatchers.allOf(
+ IntentMatchers.hasAction(Intent.ACTION_VIEW),
+ CoreMatchers.anyOf(
+ // Could be either of these, depending on whether the play store app is installed
+ IntentMatchers.hasData("market://details?id=com.instructure.teacher"),
+ IntentMatchers.hasData("https://play.google.com/store/apps/details?id=com.instructure.teacher")
+ )
+ )
+ notAParentPage.tapApp("TEACHER")
+ Intents.intended(expectedIntent)
+ } finally {
+ Intents.release()
+ }
+ }
+
+ private fun initData(): MockCanvas {
+ val data = MockCanvas.init()
+ val parent = data.addUser()
+ val course = data.addCourse()
+ data.addEnrollment(parent, course, Enrollment.EnrollmentType.Observer)
+ data.updateUserEnrollments()
+ return data
+ }
+
+ private fun goToNotAParentScreen(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent, false)
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt
new file mode 100644
index 0000000000..c1db9f2719
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.CalendarInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.User
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentCalendarInteractionTest : CalendarInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun goToCalendar(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ composeTestRule.waitForIdle()
+ }
+
+ override fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ teacherCount = 1,
+ courseCount = 2,
+ favoriteCourseCount = 1
+ )
+ }
+
+ override fun getLoggedInUser(): User {
+ return MockCanvas.data.parents[0]
+ }
+
+ override fun assertAssignmentDetailsTitle(title: String) {
+ // TODO No assertion here, this should be implemented when the Assignment Details page is created
+ }
+
+ override fun assertDiscussionDetailsTitle(title: String) {
+ // TODO No assertion here, this should be implemented when the Assignment Details page is created
+ }
+
+ override fun clickTodayButton() {
+ dashboardPage.clickTodayButton()
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateEventInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateEventInteractionTest.kt
new file mode 100644
index 0000000000..09365aedb6
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateEventInteractionTest.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.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.CreateUpdateEventInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.User
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentCreateUpdateEventInteractionTest : CreateUpdateEventInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun goToCreateEvent(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnAddButton()
+ calendarScreenPage.clickAddEvent()
+ }
+
+ override fun goToEditEvent(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ val event = data.userCalendarEvents[parent.id]!!.first()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnItem(event.title!!)
+ calendarEventDetailsPage.clickOverflowMenu()
+ calendarEventDetailsPage.clickEditMenu()
+ }
+
+ override fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ teacherCount = 0,
+ courseCount = 1,
+ favoriteCourseCount = 1
+ )
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+
+ override fun getLoggedInUser(): User = MockCanvas.data.parents[0]
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateToDoInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateToDoInteractionTest.kt
new file mode 100644
index 0000000000..a40c6cc6fd
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateToDoInteractionTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.CreateUpdateToDoInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.User
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentCreateUpdateToDoInteractionTest : CreateUpdateToDoInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun goToCreateToDo(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnAddButton()
+ calendarScreenPage.clickAddTodo()
+ }
+
+ override fun goToEditToDo(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ val todo = data.todos.first()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnItem(todo.plannable.title)
+ calendarToDoDetailsPage.clickToolbarMenu()
+ calendarToDoDetailsPage.clickEditMenu()
+ }
+
+ override fun initData(): MockCanvas {
+ val data = MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ teacherCount = 0,
+ courseCount = 1,
+ favoriteCourseCount = 1
+ )
+ data.currentUser = data.parents.first()
+ return data
+ }
+
+ override fun getLoggedInUser(): User = MockCanvas.data.parents[0]
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentEventDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentEventDetailsInteractionTest.kt
new file mode 100644
index 0000000000..e685a3520c
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentEventDetailsInteractionTest.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.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.EventDetailsInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentEventDetailsInteractionTest : EventDetailsInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun goToEventDetails(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ val event = data.courseCalendarEvents.values.first().first()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnItem(event.title!!)
+ }
+
+ override fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ teacherCount = 1,
+ courseCount = 1,
+ favoriteCourseCount = 1
+ )
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt
new file mode 100644
index 0000000000..b1d5a9aa9c
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.InboxListInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
+import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.CanvasContextPermission
+import com.instructure.canvasapi2.models.User
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.util.ParentPrefs
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+import org.junit.Before
+
+@HiltAndroidTest
+class ParentInboxListInteractionTest : InboxListInteractionTest() {
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun goToInbox(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.assertPageObjects()
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.clickInbox()
+ }
+
+ override fun createInitialData(courseCount: Int): MockCanvas {
+ val data = MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ courseCount = courseCount,
+ teacherCount = 1,
+ favoriteCourseCount = courseCount
+ )
+
+ val course1 = data.courses.values.first()
+
+ data.addCoursePermissions(
+ course1.id,
+ CanvasContextPermission(send_messages_all = true, send_messages = true)
+ )
+
+ data.addRecipientsToCourse(
+ course = course1,
+ students = data.students,
+ teachers = data.teachers
+ )
+
+ return data
+ }
+
+ override fun getLoggedInUser(): User {
+ return MockCanvas.data.parents.first()
+ }
+
+ override fun getOtherUser(): User {
+ return MockCanvas.data.teachers.first()
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentSettingsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentSettingsInteractionTest.kt
new file mode 100644
index 0000000000..e83142be59
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentSettingsInteractionTest.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.SettingsInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentSettingsInteractionTest : SettingsInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 3,
+ courseCount = 1
+ )
+ }
+
+ override fun goToSettings(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.tapSettings()
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentToDoDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentToDoDetailsInteractionTest.kt
new file mode 100644
index 0000000000..2a374c8ad9
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentToDoDetailsInteractionTest.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.parentapp.ui.interaction
+
+import android.app.Activity
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.interaction.ToDoDetailsInteractionTest
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.User
+import com.instructure.espresso.InstructureActivityTestRule
+import com.instructure.parentapp.BuildConfig
+import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.utils.ParentActivityTestRule
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+
+@HiltAndroidTest
+class ParentToDoDetailsInteractionTest : ToDoDetailsInteractionTest() {
+
+ override val isTesting = BuildConfig.IS_TESTING
+
+ override val activityRule: InstructureActivityTestRule =
+ ParentActivityTestRule(LoginActivity::class.java)
+
+ private val dashboardPage = DashboardPage()
+
+ override fun displaysPageObjects() = Unit
+
+ override fun goToToDoDetails(data: MockCanvas) {
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.clickCalendar()
+
+ val todo = data.todos.first()
+
+ composeTestRule.waitForIdle()
+ calendarScreenPage.clickOnItem(todo.plannable.title)
+ }
+
+ override fun initData(): MockCanvas {
+ return MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ teacherCount = 1,
+ courseCount = 1,
+ favoriteCourseCount = 1
+ )
+ }
+
+ override fun getLoggedInUser(): User = MockCanvas.data.parents.first()
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt
new file mode 100644
index 0000000000..09f206e617
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.parentapp.ui.pages
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.hasAnyAncestor
+import androidx.compose.ui.test.hasAnyDescendant
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.click
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onViewWithText
+
+class AlertsPage(private val composeTestRule: ComposeTestRule) : BasePage() {
+
+ fun assertAlertItemDisplayed(title: String) {
+ composeTestRule.onNodeWithText(title).assertIsDisplayed()
+ }
+
+ fun assertAlertItemNotDisplayed(title: String) {
+ composeTestRule.onNodeWithText(title).assertIsNotDisplayed()
+ }
+
+ fun assertEmptyState() {
+ composeTestRule.onNodeWithTag("emptyAlerts").assertIsDisplayed()
+ }
+
+ fun assertAlertRead(title: String) {
+ composeTestRule.onNode(
+ hasTestTag("unreadIndicator")
+ .and(hasAnyAncestor(hasTestTag("alertItem").and(hasAnyDescendant(hasText(title))))),
+ useUnmergedTree = true
+ ).assertIsNotDisplayed()
+ }
+
+ fun assertAlertUnread(title: String) {
+ composeTestRule.onNode(
+ hasTestTag("unreadIndicator")
+ .and(hasAnyAncestor(hasTestTag("alertItem").and(hasAnyDescendant(hasText(title))))),
+ useUnmergedTree = true
+ ).assertIsDisplayed()
+ }
+
+ fun dismissAlert(title: String) {
+ composeTestRule.onNode(
+ hasAnyAncestor(hasAnyDescendant(hasText(title)).and(hasTestTag("alertItem"))).and(
+ hasTestTag(
+ "dismissButton"
+ )
+ ),
+ useUnmergedTree = true
+ ).performClick()
+ }
+
+ fun clickOnAlert(title: String) {
+ composeTestRule.onNode(
+ hasTestTag("alertItem").and(hasAnyDescendant(hasText(title))),
+ useUnmergedTree = true
+ ).performClick()
+ }
+
+ fun refresh() {
+ composeTestRule.onRoot().performTouchInput { swipeDown() }
+ }
+
+ fun assertSnackbar(message: String) {
+ onViewWithText(message).assertDisplayed()
+ }
+
+ fun clickUndo() {
+ onViewWithText("UNDO").click()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt
new file mode 100644
index 0000000000..0be5021eaa
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.parentapp.ui.pages
+
+import com.instructure.canvasapi2.models.User
+import com.instructure.espresso.OnViewWithId
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.click
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.getStringFromResource
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.onViewWithId
+import com.instructure.espresso.page.onViewWithText
+import com.instructure.espresso.page.plus
+import com.instructure.espresso.page.withAncestor
+import com.instructure.espresso.page.withText
+import com.instructure.parentapp.R
+import org.hamcrest.Matchers.equalToIgnoringCase
+
+class DashboardPage : BasePage(R.id.drawer_layout) {
+
+ private val toolbar by OnViewWithId(R.id.toolbar)
+ private val bottomNavigationView by OnViewWithId(R.id.bottom_nav)
+ private val alertsItem by OnViewWithId(R.id.alerts)
+ private val calendarItem by OnViewWithId(R.id.calendar)
+
+ fun assertObserverData(user: User) {
+ onViewWithText(user.name).assertDisplayed()
+ onViewWithText(user.email.orEmpty()).assertDisplayed()
+ }
+
+ fun openNavigationDrawer() {
+ onViewWithId(R.id.navigationButtonHolder).click()
+ }
+
+ fun assertSelectedStudent(name: String) {
+ onView(withText(name) + withAncestor(R.id.selected_student_container)).assertDisplayed()
+ }
+
+ fun openStudentSelector() {
+ toolbar.click()
+ }
+
+ fun selectStudent(name: String) {
+ onView(withText(name) + withAncestor(R.id.student_list)).click()
+ }
+
+ fun tapLogout() {
+ onViewWithText(R.string.logout).click()
+ }
+
+ fun assertLogoutDialog() {
+ onViewWithText(R.string.logout_warning).assertDisplayed()
+ onViewWithText(equalToIgnoringCase(getStringFromResource(android.R.string.cancel))).assertDisplayed()
+ onViewWithText(equalToIgnoringCase(getStringFromResource(android.R.string.ok))).assertDisplayed()
+ }
+
+ fun tapOk() {
+ onViewWithText(android.R.string.ok).click()
+ }
+
+ fun tapSwitchUsers() {
+ onViewWithText(R.string.navigationDrawerSwitchUsers).click()
+ }
+
+ fun clickInbox() {
+ onViewWithText(R.string.inbox).click()
+ }
+
+ fun clickAlerts() {
+ alertsItem.click()
+ }
+
+ fun clickCalendar() {
+ calendarItem.click()
+ }
+
+ fun clickTodayButton() {
+ onViewWithId(R.id.todayButtonHolder).click()
+ }
+
+ fun tapManageStudents() {
+ onViewWithText(R.string.screenTitleManageStudents).click()
+ }
+
+ fun tapSettings() {
+ onViewWithText(R.string.settings).click()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt
new file mode 100644
index 0000000000..a1b0eb904a
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.parentapp.ui.pages
+
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasAnyChild
+import androidx.compose.ui.test.hasAnySibling
+import androidx.compose.ui.test.hasTestTag
+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.canvasapi2.models.User
+
+
+class ManageStudentsPage(private val composeTestRule: ComposeTestRule) {
+
+ fun assertStudentItemDisplayed(user: User) {
+ composeTestRule.onNodeWithText(user.shortName.orEmpty())
+ .assertIsDisplayed()
+ composeTestRule.onNode(hasTestTag("studentListItem") and hasAnyChild(hasText(user.shortName.orEmpty())), true)
+ .assertIsDisplayed()
+ .assertHasClickAction()
+ }
+
+ fun tapStudent(name: String) {
+ composeTestRule.onNodeWithText(name)
+ .assertIsDisplayed()
+ .performClick()
+ }
+
+ fun tapStudentColor(name: String) {
+ composeTestRule.onNode(hasTestTag("studentColor") and hasAnySibling(hasText(name)), true)
+ .assertIsDisplayed()
+ .performClick()
+ }
+
+ fun assertColorPickerDialogDisplayed() {
+ composeTestRule.onNodeWithText("Select Student Color")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Cancel")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("OK")
+ .assertIsDisplayed()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/NotAParentPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/NotAParentPage.kt
new file mode 100644
index 0000000000..72ba16cb88
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/NotAParentPage.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.parentapp.ui.pages
+
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+
+
+class NotAParentPage(private val composeTestRule: ComposeTestRule) {
+
+ fun expandAppOptions() {
+ composeTestRule.onNodeWithText("Are you a student or teacher?").performClick()
+ }
+
+ fun tapReturnToLogin() {
+ composeTestRule.onNodeWithText("Return to login").performClick()
+ }
+
+ fun tapApp(appName: String) {
+ composeTestRule.onNodeWithText(appName).performClick()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt
index d65bcdc917..85d6f19e0f 100644
--- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt
@@ -19,6 +19,7 @@ package com.instructure.parentapp.utils
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.AlertsPage
import org.junit.Rule
@@ -27,5 +28,7 @@ abstract class ParentComposeTest : ParentTest() {
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule()
+ protected val alertsPage = AlertsPage(composeTestRule)
+
override fun displaysPageObjects() = Unit
}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt
index 852cf6a357..1d1a2b3d2f 100644
--- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt
@@ -20,6 +20,7 @@ package com.instructure.parentapp.utils
import com.instructure.canvas.espresso.CanvasTest
import com.instructure.parentapp.BuildConfig
import com.instructure.parentapp.features.login.LoginActivity
+import com.instructure.parentapp.ui.pages.DashboardPage
abstract class ParentTest : CanvasTest() {
@@ -27,4 +28,6 @@ abstract class ParentTest : CanvasTest() {
override val isTesting = BuildConfig.IS_TESTING
override val activityRule = ParentActivityTestRule(LoginActivity::class.java)
+
+ val dashboardPage = DashboardPage()
}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt
index ca51ca25b8..99c8e6583b 100644
--- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt
@@ -17,15 +17,15 @@
package com.instructure.parentapp.utils
-import androidx.test.espresso.assertion.ViewAssertions
-import androidx.test.espresso.matcher.ViewMatchers
-import com.instructure.canvas.espresso.waitForMatcherWithSleeps
+import androidx.annotation.DrawableRes
+import androidx.compose.ui.test.SemanticsMatcher
+import com.instructure.canvas.espresso.CanvasTest
import com.instructure.canvasapi2.models.User
-import com.instructure.parentapp.R
+import com.instructure.pandautils.utils.DrawableId
import com.instructure.parentapp.features.login.LoginActivity
-fun ParentTest.tokenLogin(domain: String, token: String, user: User) {
+fun CanvasTest.tokenLogin(domain: String, token: String, user: User, assertDashboard: Boolean = true) {
activityRule.runOnUiThread {
(originalActivity as LoginActivity).loginWithToken(
token,
@@ -34,5 +34,10 @@ fun ParentTest.tokenLogin(domain: String, token: String, user: User) {
)
}
- waitForMatcherWithSleeps(ViewMatchers.withId(R.id.toolbar), 20000).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
+ if (assertDashboard && this is ParentTest) {
+ dashboardPage.assertPageObjects()
+ }
}
+
+fun hasDrawable(@DrawableRes id: Int): SemanticsMatcher =
+ SemanticsMatcher.expectValue(DrawableId, id)
diff --git a/apps/parent/src/main/AndroidManifest.xml b/apps/parent/src/main/AndroidManifest.xml
index 2596ab4229..14ab33d460 100644
--- a/apps/parent/src/main/AndroidManifest.xml
+++ b/apps/parent/src/main/AndroidManifest.xml
@@ -20,8 +20,8 @@
+ android:maxSdkVersion="29"
+ tools:replace="android:maxSdkVersion" />
@@ -120,9 +121,7 @@
android:exported="false"
android:label="@string/canvas"
android:launchMode="singleTask"
- android:theme="@style/CanvasMaterialTheme_Default">
-
-
+ android:theme="@style/CanvasMaterialTheme_Default" />
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/AlertsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/AlertsModule.kt
new file mode 100644
index 0000000000..23e6953d61
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/AlertsModule.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.parentapp.di
+
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.ObserverApi
+import com.instructure.parentapp.features.alerts.list.AlertsRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class AlertsModule {
+
+ @Provides
+ fun provideAlertsRepository(observerApi: ObserverApi, courseApi: CourseAPI.CoursesInterface): AlertsRepository {
+ return AlertsRepository(observerApi, courseApi)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt
index cf556d6c79..7d0f341cf6 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt
@@ -18,15 +18,18 @@
package com.instructure.parentapp.di
import com.instructure.canvasapi2.utils.Analytics
+import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.loginapi.login.util.PreviousUsersUtils
import com.instructure.loginapi.login.util.QRLogin
import com.instructure.pandautils.utils.LogoutHelper
import com.instructure.parentapp.util.ParentLogoutHelper
import com.instructure.parentapp.util.ParentPrefs
+import com.instructure.parentapp.util.navigation.Navigation
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@@ -56,4 +59,10 @@ class ApplicationModule {
fun provideParentPrefs(): ParentPrefs {
return ParentPrefs
}
+
+ @Provides
+ @Singleton
+ fun provideNavigation(apiPrefs: ApiPrefs): Navigation {
+ return Navigation(apiPrefs)
+ }
}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/DatabaseModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/DatabaseModule.kt
index 3bbb7e5780..1d1e147775 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/DatabaseModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/DatabaseModule.kt
@@ -22,6 +22,7 @@ import androidx.room.Room
import com.instructure.pandautils.room.appdatabase.AppDatabase
import com.instructure.pandautils.room.appdatabase.appDatabaseMigrations
import com.instructure.pandautils.room.calendar.CalendarFilterDatabase
+import com.instructure.pandautils.room.calendar.calendarDatabaseMigrations
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -44,7 +45,8 @@ class DatabaseModule {
@Provides
@Singleton
fun provideCalendarDatabase(@ApplicationContext context: Context): CalendarFilterDatabase {
- // TODO: Implement
- throw NotImplementedError()
+ return Room.databaseBuilder(context, CalendarFilterDatabase::class.java, "db-calendar-parent")
+ .addMigrations(*calendarDatabaseMigrations)
+ .build()
}
}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/FragmentModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/FragmentModule.kt
index e192fe3f2b..07bffd2025 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/FragmentModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/FragmentModule.kt
@@ -19,6 +19,7 @@ package com.instructure.parentapp.di
import androidx.fragment.app.FragmentActivity
import com.instructure.pandautils.navigation.WebViewRouter
+import com.instructure.parentapp.util.navigation.ParentWebViewRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -33,7 +34,6 @@ class FragmentModule {
@Provides
fun provideWebViewRouter(activity: FragmentActivity): WebViewRouter {
- // TODO: Implement
- throw NotImplementedError()
+ return ParentWebViewRouter(activity)
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/LegalModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/LegalModule.kt
new file mode 100644
index 0000000000..e99ad42ecd
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/LegalModule.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.parentapp.di
+
+import android.app.Activity
+import com.instructure.pandautils.features.legal.LegalRouter
+import com.instructure.parentapp.features.legal.ParentLegalRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+
+@Module
+@InstallIn(ActivityComponent::class)
+class LegalModule {
+
+ @Provides
+ fun provideLegalRouter(activity: Activity): LegalRouter {
+ return ParentLegalRouter(activity)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/ManageStudentsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/ManageStudentsModule.kt
new file mode 100644
index 0000000000..8b6ccebd9e
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/ManageStudentsModule.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.parentapp.di
+
+import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.apis.UserAPI
+import com.instructure.parentapp.features.managestudents.ManageStudentsRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class ManageStudentsModule {
+
+ @Provides
+ fun provideManageStudentsRepository(
+ enrollmentsApi: EnrollmentAPI.EnrollmentInterface,
+ userApi: UserAPI.UsersInterface
+ ): ManageStudentsRepository {
+ return ManageStudentsRepository(enrollmentsApi, userApi)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/SettingsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/SettingsModule.kt
new file mode 100644
index 0000000000..f706dbb0b0
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/SettingsModule.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.parentapp.di
+
+import com.instructure.pandautils.features.settings.SettingsBehaviour
+import com.instructure.pandautils.features.settings.SettingsRouter
+import com.instructure.parentapp.features.settings.ParentSettingsBehaviour
+import com.instructure.parentapp.features.settings.ParentSettingsRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+class SettingsModule {
+
+ @Provides
+ fun provideSettingsBehaviour(): SettingsBehaviour {
+ return ParentSettingsBehaviour()
+ }
+
+ @Provides
+ fun provideSettingsRouter(): SettingsRouter {
+ return ParentSettingsRouter()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/AboutModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AboutModule.kt
similarity index 87%
rename from apps/parent/src/main/java/com/instructure/parentapp/di/AboutModule.kt
rename to apps/parent/src/main/java/com/instructure/parentapp/di/feature/AboutModule.kt
index cb0a693868..c93501e8b1 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/AboutModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AboutModule.kt
@@ -15,11 +15,12 @@
*
*/
-package com.instructure.parentapp.di
+package com.instructure.parentapp.di.feature
import android.content.Context
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.pandautils.features.about.AboutRepository
+import com.instructure.parentapp.features.about.ParentAboutRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -35,7 +36,6 @@ class AboutModule {
@ApplicationContext context: Context,
apiPrefs: ApiPrefs
): AboutRepository {
- // TODO: Implement
- throw NotImplementedError()
+ return ParentAboutRepository(context, apiPrefs)
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/CalendarModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CalendarModule.kt
similarity index 60%
rename from apps/parent/src/main/java/com/instructure/parentapp/di/CalendarModule.kt
rename to apps/parent/src/main/java/com/instructure/parentapp/di/feature/CalendarModule.kt
index 42209d6694..9f8aa5efdc 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/CalendarModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CalendarModule.kt
@@ -14,15 +14,21 @@
* along with this program. If not, see .
*
*/
-package com.instructure.parentapp.di
+package com.instructure.parentapp.di.feature
import androidx.fragment.app.FragmentActivity
+import com.instructure.canvasapi2.apis.CalendarEventAPI
import com.instructure.canvasapi2.apis.CourseAPI
-import com.instructure.canvasapi2.apis.GroupAPI
+import com.instructure.canvasapi2.apis.FeaturesAPI
import com.instructure.canvasapi2.apis.PlannerAPI
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.pandautils.features.calendar.CalendarRepository
import com.instructure.pandautils.features.calendar.CalendarRouter
+import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao
+import com.instructure.parentapp.features.calendar.ParentCalendarRepository
+import com.instructure.parentapp.features.calendar.ParentCalendarRouter
+import com.instructure.parentapp.util.ParentPrefs
+import com.instructure.parentapp.util.navigation.Navigation
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -34,9 +40,8 @@ import dagger.hilt.android.components.ViewModelComponent
class CalendarModule {
@Provides
- fun provideCalendarRouter(activity: FragmentActivity): CalendarRouter {
- // TODO: Implement
- throw NotImplementedError()
+ fun provideCalendarRouter(activity: FragmentActivity, navigation: Navigation): CalendarRouter {
+ return ParentCalendarRouter(activity, navigation)
}
}
@@ -48,10 +53,12 @@ class CalendarViewModelModule {
fun provideCalendarRepository(
plannerApi: PlannerAPI.PlannerInterface,
coursesApi: CourseAPI.CoursesInterface,
- groupsApi: GroupAPI.GroupInterface,
- apiPrefs: ApiPrefs
+ calendarEventsApi: CalendarEventAPI.CalendarEventInterface,
+ apiPrefs: ApiPrefs,
+ featuresApi: FeaturesAPI.FeaturesInterface,
+ parentPrefs: ParentPrefs,
+ calendarFilterDao: CalendarFilterDao
): CalendarRepository {
- // TODO: Implement
- throw NotImplementedError()
+ return ParentCalendarRepository(plannerApi, coursesApi, calendarEventsApi, apiPrefs, featuresApi, parentPrefs, calendarFilterDao)
}
}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/CoursesModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CoursesModule.kt
similarity index 97%
rename from apps/parent/src/main/java/com/instructure/parentapp/di/CoursesModule.kt
rename to apps/parent/src/main/java/com/instructure/parentapp/di/feature/CoursesModule.kt
index 5c946fb125..0a6dd85b9a 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/CoursesModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CoursesModule.kt
@@ -15,7 +15,7 @@
*
*/
-package com.instructure.parentapp.di
+package com.instructure.parentapp.di.feature
import android.content.Context
import com.instructure.canvasapi2.apis.CourseAPI
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/CreateUpdateEventModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateEventModule.kt
similarity index 81%
rename from apps/parent/src/main/java/com/instructure/parentapp/di/CreateUpdateEventModule.kt
rename to apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateEventModule.kt
index 819296f20e..9a4e3933a8 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/CreateUpdateEventModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateEventModule.kt
@@ -15,13 +15,12 @@
*
*/
-package com.instructure.parentapp.di
+package com.instructure.parentapp.di.feature
import com.instructure.canvasapi2.apis.CalendarEventAPI
-import com.instructure.canvasapi2.apis.CourseAPI
-import com.instructure.canvasapi2.apis.GroupAPI
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.pandautils.features.calendarevent.createupdate.CreateUpdateEventRepository
+import com.instructure.parentapp.features.calendarevent.ParentCreateUpdateEventRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -35,11 +34,8 @@ class CreateUpdateEventModule {
@Provides
fun provideCreateUpdateEventRepository(
calendarEventApi: CalendarEventAPI.CalendarEventInterface,
- coursesApi: CourseAPI.CoursesInterface,
- groupsApi: GroupAPI.GroupInterface,
apiPrefs: ApiPrefs
): CreateUpdateEventRepository {
- // TODO: Implement
- throw NotImplementedError()
+ return ParentCreateUpdateEventRepository(calendarEventApi, apiPrefs)
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateToDoModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateToDoModule.kt
new file mode 100644
index 0000000000..756137158f
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/CreateUpdateToDoModule.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.parentapp.di.feature
+
+import com.instructure.canvasapi2.apis.CalendarEventAPI
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.PlannerAPI
+import com.instructure.canvasapi2.di.PLANNER_API_SERIALIZE_NULLS
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.calendarevent.createupdate.CreateUpdateEventRepository
+import com.instructure.pandautils.features.calendartodo.createupdate.CreateUpdateToDoRepository
+import com.instructure.parentapp.features.calendarevent.ParentCreateUpdateEventRepository
+import com.instructure.parentapp.features.calendartodo.ParentCreateUpdateToDoRepository
+import com.instructure.parentapp.util.ParentPrefs
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import javax.inject.Named
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class CreateUpdateToDoModule {
+
+ @Provides
+ fun provideCreateUpdateToDoRepository(
+ coursesApi: CourseAPI.CoursesInterface,
+ parentPrefs: ParentPrefs,
+ @Named(PLANNER_API_SERIALIZE_NULLS) plannerApi: PlannerAPI.PlannerInterface
+ ): CreateUpdateToDoRepository {
+ return ParentCreateUpdateToDoRepository(coursesApi, parentPrefs, plannerApi)
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/DashboardModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/DashboardModule.kt
new file mode 100644
index 0000000000..4c575ef2a6
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/DashboardModule.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.parentapp.di.feature
+
+import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.apis.UnreadCountAPI
+import com.instructure.parentapp.features.dashboard.AlertCountUpdater
+import com.instructure.parentapp.features.dashboard.AlertCountUpdaterImpl
+import com.instructure.parentapp.features.dashboard.DashboardRepository
+import com.instructure.parentapp.features.dashboard.InboxCountUpdater
+import com.instructure.parentapp.features.dashboard.InboxCountUpdaterImpl
+import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
+import com.instructure.parentapp.features.dashboard.SelectedStudentHolderImpl
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class DashboardModule {
+
+ @Provides
+ fun provideDashboardRepository(
+ enrollmentApi: EnrollmentAPI.EnrollmentInterface,
+ unreadCountsApi: UnreadCountAPI.UnreadCountsInterface
+ ): DashboardRepository {
+ return DashboardRepository(enrollmentApi, unreadCountsApi)
+ }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class SelectedStudentHolderModule {
+
+ @Provides
+ @Singleton
+ fun provideSelectedStudentHolder(): SelectedStudentHolder {
+ return SelectedStudentHolderImpl()
+ }
+
+ @Provides
+ @Singleton
+ fun provideInboxCountUpdater(): InboxCountUpdater {
+ return InboxCountUpdaterImpl()
+ }
+
+ @Provides
+ @Singleton
+ fun provideAlertCountUpdater(): AlertCountUpdater {
+ return AlertCountUpdaterImpl()
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/EventModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/EventModule.kt
similarity index 76%
rename from apps/parent/src/main/java/com/instructure/parentapp/di/EventModule.kt
rename to apps/parent/src/main/java/com/instructure/parentapp/di/feature/EventModule.kt
index 776a7c9e8f..cfcb31ec97 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/EventModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/EventModule.kt
@@ -15,10 +15,12 @@
*
*/
-package com.instructure.parentapp.di
+package com.instructure.parentapp.di.feature
import androidx.fragment.app.FragmentActivity
import com.instructure.pandautils.features.calendarevent.details.EventRouter
+import com.instructure.parentapp.features.calendarevent.ParentEventRouter
+import com.instructure.parentapp.util.navigation.Navigation
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -29,8 +31,7 @@ import dagger.hilt.android.components.FragmentComponent
class EventModule {
@Provides
- fun provideEventRouter(activity: FragmentActivity): EventRouter {
- // TODO: Implement
- throw NotImplementedError()
+ fun provideEventRouter(activity: FragmentActivity, navigation: Navigation): EventRouter {
+ return ParentEventRouter(activity, navigation)
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/HelpDialogModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/HelpDialogModule.kt
similarity index 97%
rename from apps/parent/src/main/java/com/instructure/parentapp/di/HelpDialogModule.kt
rename to apps/parent/src/main/java/com/instructure/parentapp/di/feature/HelpDialogModule.kt
index 98764dd198..3467d30c71 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/HelpDialogModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/HelpDialogModule.kt
@@ -15,7 +15,7 @@
*
*/
-package com.instructure.parentapp.di
+package com.instructure.parentapp.di.feature
import androidx.fragment.app.FragmentActivity
import com.instructure.pandautils.features.help.HelpDialogFragmentBehavior
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/InboxModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/InboxModule.kt
similarity index 97%
rename from apps/parent/src/main/java/com/instructure/parentapp/di/InboxModule.kt
rename to apps/parent/src/main/java/com/instructure/parentapp/di/feature/InboxModule.kt
index c7a0f29456..7b6dc43d5b 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/InboxModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/InboxModule.kt
@@ -15,7 +15,7 @@
*
*/
-package com.instructure.parentapp.di
+package com.instructure.parentapp.di.feature
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/LoginModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LoginModule.kt
similarity index 88%
rename from apps/parent/src/main/java/com/instructure/parentapp/di/LoginModule.kt
rename to apps/parent/src/main/java/com/instructure/parentapp/di/feature/LoginModule.kt
index 0f31c84cb2..8880972b59 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/LoginModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LoginModule.kt
@@ -15,11 +15,12 @@
*
*/
-package com.instructure.parentapp.di
+package com.instructure.parentapp.di.feature
import androidx.fragment.app.FragmentActivity
import com.instructure.loginapi.login.LoginNavigation
import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter
+import com.instructure.parentapp.features.login.ParentAcceptableUsePolicyRouter
import com.instructure.parentapp.features.login.ParentLoginNavigation
import dagger.Module
import dagger.Provides
@@ -32,8 +33,7 @@ class LoginModule {
@Provides
fun provideAcceptableUsePolicyRouter(activity: FragmentActivity): AcceptableUsePolicyRouter {
- // TODO: Implement
- throw NotImplementedError()
+ return ParentAcceptableUsePolicyRouter(activity)
}
@Provides
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/MainModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SplashModule.kt
similarity index 57%
rename from apps/parent/src/main/java/com/instructure/parentapp/di/MainModule.kt
rename to apps/parent/src/main/java/com/instructure/parentapp/di/feature/SplashModule.kt
index 1a922e07a5..fa964f67de 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/MainModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SplashModule.kt
@@ -15,42 +15,28 @@
*
*/
-package com.instructure.parentapp.di
+package com.instructure.parentapp.di.feature
import com.instructure.canvasapi2.apis.EnrollmentAPI
import com.instructure.canvasapi2.apis.ThemeAPI
import com.instructure.canvasapi2.apis.UserAPI
-import com.instructure.parentapp.features.main.MainRepository
-import com.instructure.parentapp.features.main.SelectedStudentHolder
-import com.instructure.parentapp.features.main.SelectedStudentHolderImpl
+import com.instructure.parentapp.features.splash.SplashRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
+
@Module
@InstallIn(ViewModelComponent::class)
-class MainModule {
+class SplashModule {
@Provides
- fun provideMainRepository(
- enrollmentApi: EnrollmentAPI.EnrollmentInterface,
+ fun provideSplashRepository(
userApi: UserAPI.UsersInterface,
- themeApi: ThemeAPI.ThemeInterface
- ): MainRepository {
- return MainRepository(enrollmentApi, userApi, themeApi)
- }
-}
-
-@Module
-@InstallIn(SingletonComponent::class)
-class SelectedStudentHolderModule {
-
- @Provides
- @Singleton
- fun provideSelectedStudentHolder(): SelectedStudentHolder {
- return SelectedStudentHolderImpl()
+ themeApi: ThemeAPI.ThemeInterface,
+ enrollmentApi: EnrollmentAPI.EnrollmentInterface
+ ): SplashRepository {
+ return SplashRepository(userApi, themeApi, enrollmentApi)
}
-}
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/ToDoModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/ToDoModule.kt
similarity index 76%
rename from apps/parent/src/main/java/com/instructure/parentapp/di/ToDoModule.kt
rename to apps/parent/src/main/java/com/instructure/parentapp/di/feature/ToDoModule.kt
index dccd3a77c3..dd699ea531 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/ToDoModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/ToDoModule.kt
@@ -15,10 +15,12 @@
*
*/
-package com.instructure.parentapp.di
+package com.instructure.parentapp.di.feature
import androidx.fragment.app.FragmentActivity
import com.instructure.pandautils.features.calendartodo.details.ToDoRouter
+import com.instructure.parentapp.features.calendartodo.ParentToDoRouter
+import com.instructure.parentapp.util.navigation.Navigation
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -29,8 +31,7 @@ import dagger.hilt.android.components.FragmentComponent
class ToDoModule {
@Provides
- fun provideToDoRouter(activity: FragmentActivity): ToDoRouter {
- // TODO: Implement
- throw NotImplementedError()
+ fun provideToDoRouter(activity: FragmentActivity, navigation: Navigation): ToDoRouter {
+ return ParentToDoRouter(activity, navigation)
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/about/ParentAboutRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/about/ParentAboutRepository.kt
new file mode 100644
index 0000000000..ca1c822b1d
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/about/ParentAboutRepository.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.parentapp.features.about
+
+import android.content.Context
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.about.AboutRepository
+import com.instructure.parentapp.BuildConfig
+
+class ParentAboutRepository(context: Context, apiPrefs: ApiPrefs) :
+ AboutRepository(context, apiPrefs) {
+
+ override val appVersion: String = BuildConfig.VERSION_NAME
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt
index d34b8534cc..74a2c9bdf0 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt
@@ -21,13 +21,53 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.snackbar.Snackbar
+import com.instructure.pandautils.utils.collectOneOffEvents
import com.instructure.parentapp.R
+import com.instructure.parentapp.util.navigation.Navigation
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
-
+@AndroidEntryPoint
class AlertsFragment : Fragment() {
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
- return layoutInflater.inflate(R.layout.fragment_alerts, container, false)
+ @Inject
+ lateinit var navigation: Navigation
+
+ private val viewModel: AlertsViewModel by viewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ lifecycleScope.collectOneOffEvents(viewModel.events, ::handleAction)
+ return ComposeView(requireActivity()).apply {
+ setContent {
+ val uiState by viewModel.uiState.collectAsState()
+ AlertsScreen(uiState = uiState, actionHandler = viewModel::handleAction)
+ }
+ }
+ }
+
+ private fun handleAction(action: AlertsViewModelAction) {
+ when (action) {
+ is AlertsViewModelAction.Navigate -> {
+ navigation.navigate(activity, action.route)
+ }
+
+ is AlertsViewModelAction.ShowSnackbar -> {
+ Snackbar.make(requireView(), action.message, Snackbar.LENGTH_SHORT).apply {
+ action.action?.let { setAction(it) { action.actionCallback?.invoke() } }
+ setActionTextColor(resources.getColor(R.color.white, resources.newTheme()))
+ }.show()
+ }
+ }
}
}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt
new file mode 100644
index 0000000000..70b932d0cf
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.parentapp.features.alerts.list
+
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.ObserverApi
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.Alert
+import com.instructure.canvasapi2.models.AlertThreshold
+import com.instructure.canvasapi2.models.AlertWorkflowState
+import com.instructure.canvasapi2.models.CourseSettings
+import com.instructure.canvasapi2.utils.depaginate
+
+class AlertsRepository(
+ private val observerApi: ObserverApi,
+ private val courseApi: CourseAPI.CoursesInterface
+) {
+
+ suspend fun getAlertsForStudent(studentId: Long, forceNetwork: Boolean): List {
+ val restParams = RestParams(isForceReadFromNetwork = forceNetwork)
+ val allAlerts = observerApi.getObserverAlerts(studentId, restParams).depaginate {
+ observerApi.getNextPageObserverAlerts(it, restParams)
+ }.dataOrThrow.sortedByDescending { it.actionDate }
+
+ val coursesMap = mutableMapOf()
+ val filteredAlerts = allAlerts.filter { alert ->
+ if (!alert.isQuantitativeRestrictionApplies()) return@filter true
+
+ alert.getCourseId()?.let { courseId ->
+ val settings = coursesMap.getOrPut(courseId) {
+ courseApi.getCourseSettings(courseId, restParams).dataOrNull
+ }
+ settings?.restrictQuantitativeData?.not() ?: true
+ } ?: true
+ }
+
+ return filteredAlerts
+ }
+
+ suspend fun getAlertThresholdForStudent(
+ studentId: Long,
+ forceNetwork: Boolean
+ ): List {
+ val restParams = RestParams(isForceReadFromNetwork = forceNetwork)
+ return observerApi.getObserverAlertThresholds(studentId, restParams).dataOrNull
+ ?: emptyList()
+ }
+
+ suspend fun updateAlertWorkflow(alertId: Long, workflowState: AlertWorkflowState): Alert {
+ val restParams = RestParams(isForceReadFromNetwork = true)
+ return observerApi.updateAlertWorkflow(
+ alertId,
+ workflowState.name.lowercase(),
+ restParams
+ ).dataOrThrow
+ }
+
+ suspend fun getUnreadAlertCount(studentId: Long): Int {
+ val alerts = try {
+ getAlertsForStudent(studentId, true)
+ } catch (e: Exception) {
+ emptyList()
+ }
+ return alerts.count { it.workflowState == AlertWorkflowState.UNREAD }
+ }
+
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt
new file mode 100644
index 0000000000..01739d777d
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt
@@ -0,0 +1,441 @@
+/*
+ * 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 .
+ *
+ */
+@file:OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
+
+package com.instructure.parentapp.features.alerts.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.pullrefresh.PullRefreshIndicator
+import androidx.compose.material.pullrefresh.pullRefresh
+import androidx.compose.material.pullrefresh.rememberPullRefreshState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.instructure.canvasapi2.models.AlertType
+import com.instructure.canvasapi2.utils.ContextKeeper
+import com.instructure.canvasapi2.utils.DateHelper
+import com.instructure.pandautils.R
+import com.instructure.pandautils.compose.CanvasTheme
+import com.instructure.pandautils.compose.composables.EmptyContent
+import com.instructure.pandautils.compose.composables.ErrorContent
+import com.instructure.pandautils.compose.composables.Loading
+import com.instructure.pandautils.utils.drawableId
+import java.util.Date
+
+
+@Composable
+fun AlertsScreen(
+ uiState: AlertsUiState,
+ actionHandler: (AlertsAction) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ CanvasTheme {
+ Scaffold(
+ backgroundColor = colorResource(id = R.color.backgroundLightest),
+ content = { padding ->
+ val pullRefreshState = rememberPullRefreshState(
+ refreshing = uiState.isRefreshing,
+ onRefresh = {
+ actionHandler(AlertsAction.Refresh)
+ }
+ )
+ Box(modifier = modifier.pullRefresh(pullRefreshState)) {
+ when {
+ uiState.isError -> {
+ ErrorContent(
+ errorMessage = stringResource(id = R.string.errorLoadingAlerts),
+ retryClick = {
+ actionHandler(AlertsAction.Refresh)
+ }, modifier = Modifier.fillMaxSize()
+ )
+ }
+
+ uiState.isLoading -> {
+ Loading(
+ modifier = Modifier
+ .fillMaxSize()
+ .testTag("loading"),
+ color = Color(uiState.studentColor)
+ )
+ }
+
+ uiState.alerts.isEmpty() -> {
+ EmptyContent(
+ emptyTitle = stringResource(id = R.string.parentNoAlerts),
+ emptyMessage = stringResource(id = R.string.parentNoAlersMessage),
+ imageRes = R.drawable.ic_panda_noalerts,
+ modifier = Modifier
+ .fillMaxSize()
+ .testTag("emptyAlerts")
+ .verticalScroll(rememberScrollState())
+ )
+ }
+
+ else -> {
+ AlertsListContent(
+ uiState = uiState,
+ actionHandler = actionHandler,
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize()
+ )
+ }
+ }
+ PullRefreshIndicator(
+ refreshing = uiState.isRefreshing,
+ state = pullRefreshState,
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .testTag("pullRefreshIndicator"),
+ contentColor = Color(uiState.studentColor)
+ )
+ }
+
+ },
+ modifier = modifier
+ )
+ }
+}
+
+@Composable
+fun AlertsListContent(
+ uiState: AlertsUiState,
+ actionHandler: (AlertsAction) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ LazyColumn(
+ modifier = modifier
+ ) {
+ items(uiState.alerts, key = { it.alertId }) { alert ->
+ AlertsListItem(
+ alert = alert,
+ userColor = uiState.studentColor,
+ actionHandler = actionHandler,
+ modifier = Modifier.animateItemPlacement()
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ }
+ }
+}
+
+@Composable
+fun AlertsListItem(
+ alert: AlertsItemUiState,
+ userColor: Int,
+ actionHandler: (AlertsAction) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+
+ fun alertTitle(alertType: AlertType, alertThreshold: String?): String {
+ val threshold = alertThreshold.orEmpty()
+ return when (alertType) {
+ AlertType.ASSIGNMENT_MISSING -> context.getString(R.string.assignmentMissingAlertTitle)
+ AlertType.ASSIGNMENT_GRADE_HIGH -> context.getString(
+ R.string.assignmentGradeHighAlertTitle,
+ threshold
+ )
+ AlertType.ASSIGNMENT_GRADE_LOW -> context.getString(
+ R.string.assignmentGradeLowAlertTitle,
+ threshold
+ )
+ AlertType.COURSE_GRADE_HIGH -> context.getString(
+ R.string.courseGradeHighAlertTitle,
+ threshold
+ )
+ AlertType.COURSE_GRADE_LOW -> context.getString(
+ R.string.courseGradeLowAlertTitle,
+ threshold
+ )
+ AlertType.COURSE_ANNOUNCEMENT -> context.getString(R.string.courseAnnouncementAlertTitle)
+ AlertType.INSTITUTION_ANNOUNCEMENT -> context.getString(R.string.institutionAnnouncementAlertTitle)
+ }
+ }
+
+ fun alertIcon(alertType: AlertType, lockedForUser: Boolean): Int {
+ return when {
+ lockedForUser -> R.drawable.ic_lock_lined
+ alertType.isAlertInfo() || alertType.isAlertPositive() -> R.drawable.ic_info
+ alertType.isAlertNegative() -> R.drawable.ic_warning
+ else -> R.drawable.ic_info
+ }
+ }
+
+ fun alertColor(alertType: AlertType): Int {
+ return when {
+ alertType.isAlertInfo() -> context.getColor(R.color.textDark)
+ alertType.isAlertNegative() -> context.getColor(R.color.textDanger)
+ alertType.isAlertPositive() -> userColor
+ else -> context.getColor(R.color.textDark)
+ }
+ }
+
+ fun dateTime(dateTime: Date): String {
+ val date = DateHelper.getDayMonthDateString(context, dateTime)
+ val time = DateHelper.getFormattedTime(context, dateTime)
+
+ return context.getString(R.string.alertDateTime, date, time)
+ }
+
+ Row(modifier = modifier
+ .fillMaxWidth()
+ .clickable(enabled = alert.htmlUrl != null) {
+ alert.htmlUrl?.let {
+ actionHandler(AlertsAction.Navigate(alert.alertId, it))
+ }
+ }
+ .padding(8.dp)
+ .testTag("alertItem"),
+ verticalAlignment = Alignment.CenterVertically) {
+ Row(modifier = Modifier.align(Alignment.Top)) {
+ if (alert.unread) {
+ Box(
+ modifier = Modifier
+ .size(8.dp)
+ .clip(CircleShape)
+ .background(Color(userColor))
+ .testTag("unreadIndicator")
+ )
+ }
+
+ val iconId = alertIcon(alert.alertType, alert.lockedForUser)
+ Icon(
+ modifier = Modifier
+ .padding(start = if (alert.unread) 0.dp else 8.dp, end = 32.dp)
+ .semantics {
+ drawableId = iconId
+ },
+ painter = painterResource(id = iconId),
+ contentDescription = null,
+ tint = Color(alertColor(alert.alertType))
+ )
+ }
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = alertTitle(alert.alertType, alert.observerAlertThreshold),
+ style = TextStyle(color = Color(alertColor(alert.alertType)), fontSize = 12.sp)
+ )
+ Text(
+ modifier = Modifier.padding(vertical = 4.dp),
+ text = alert.title,
+ style = TextStyle(color = colorResource(id = R.color.textDarkest), fontSize = 16.sp)
+ )
+ alert.date?.let {
+ Text(
+ text = dateTime(alert.date),
+ style = TextStyle(
+ color = colorResource(id = R.color.textDark),
+ fontSize = 12.sp
+ )
+ )
+ }
+ }
+ IconButton(
+ modifier = Modifier
+ .testTag("dismissButton"),
+ onClick = { actionHandler(AlertsAction.DismissAlert(alert.alertId)) }) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_close),
+ tint = colorResource(id = R.color.textDark),
+ contentDescription = stringResource(
+ id = R.string.a11y_contentDescription_observerAlertDelete
+ )
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun AlertsScreenPreview() {
+ AlertsScreen(
+ uiState = AlertsUiState(
+ alerts = listOf(
+ AlertsItemUiState(
+ alertId = 1L,
+ title = "Alert title",
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 2L,
+ title = "Assignment missing",
+ alertType = AlertType.ASSIGNMENT_MISSING,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 3L,
+ title = "Course grade low",
+ alertType = AlertType.COURSE_GRADE_LOW,
+ date = Date(),
+ observerAlertThreshold = "8",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 4L,
+ title = "Course grade high",
+ alertType = AlertType.COURSE_GRADE_HIGH,
+ date = Date(),
+ observerAlertThreshold = "80%",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 5L,
+ title = "Institution announcement",
+ alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 6L,
+ title = "Assignment grade low",
+ alertType = AlertType.ASSIGNMENT_GRADE_LOW,
+ date = Date(),
+ observerAlertThreshold = "8",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 7L,
+ title = "Assignment grade high",
+ alertType = AlertType.ASSIGNMENT_GRADE_HIGH,
+ date = Date(),
+ observerAlertThreshold = "80%",
+ lockedForUser = false,
+ unread = false,
+ htmlUrl = ""
+ ),
+ AlertsItemUiState(
+ alertId = 8L,
+ title = "Locked alert",
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = true,
+ unread = false,
+ htmlUrl = ""
+ )
+ )
+ ),
+ actionHandler = {}
+ )
+}
+
+@Preview
+@Composable
+fun AlertsScreenErrorPreview() {
+ AlertsScreen(
+ uiState = AlertsUiState(isError = true),
+ actionHandler = {}
+ )
+}
+
+@Preview
+@Composable
+fun AlertsScreenEmptyPreview() {
+ AlertsScreen(
+ uiState = AlertsUiState(),
+ actionHandler = {}
+ )
+}
+
+@Preview
+@Composable
+fun AlertsScreenLoadingPreview() {
+ ContextKeeper.appContext = LocalContext.current
+ AlertsScreen(
+ uiState = AlertsUiState(isLoading = true),
+ actionHandler = {}
+ )
+}
+
+@Preview
+@Composable
+fun AlertsScreenRefreshingPreview() {
+ AlertsScreen(
+ uiState = AlertsUiState(isRefreshing = true),
+ actionHandler = {}
+ )
+}
+
+@Preview
+@Composable
+fun AlertsListItemPreview() {
+ AlertsListItem(
+ alert = AlertsItemUiState(
+ alertId = 1L,
+ title = "Alert title",
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
+ date = Date(),
+ observerAlertThreshold = null,
+ lockedForUser = false,
+ unread = true,
+ htmlUrl = ""
+ ),
+ userColor = Color.Blue.toArgb(),
+ actionHandler = {}
+ )
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt
new file mode 100644
index 0000000000..f9199fcd11
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.parentapp.features.alerts.list
+
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import com.instructure.canvasapi2.models.AlertType
+import java.util.Date
+
+data class AlertsUiState(
+ val alerts: List = emptyList(),
+ @ColorInt val studentColor: Int = Color.BLACK,
+ val isLoading: Boolean = false,
+ val isError: Boolean = false,
+ val isRefreshing: Boolean = false
+)
+
+data class AlertsItemUiState(
+ val alertId: Long,
+ val title: String,
+ val alertType: AlertType,
+ val date: Date?,
+ val observerAlertThreshold: String?,
+ val lockedForUser: Boolean,
+ val unread: Boolean,
+ val htmlUrl: String?
+)
+
+sealed class AlertsViewModelAction {
+ data class Navigate(val route: String): AlertsViewModelAction()
+ data class ShowSnackbar(val message: Int, val action: Int?, val actionCallback: (() -> Unit)?): AlertsViewModelAction()
+}
+
+sealed class AlertsAction {
+ data object Refresh : AlertsAction()
+ data class Navigate(val alertId: Long, val route: String) : AlertsAction()
+ data class DismissAlert(val alertId: Long) : AlertsAction()
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt
new file mode 100644
index 0000000000..84ee8ac7a9
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.parentapp.features.alerts.list
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.instructure.canvasapi2.models.Alert
+import com.instructure.canvasapi2.models.AlertThreshold
+import com.instructure.canvasapi2.models.AlertWorkflowState
+import com.instructure.canvasapi2.models.User
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.parentapp.R
+import com.instructure.parentapp.features.dashboard.AlertCountUpdater
+import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class AlertsViewModel @Inject constructor(
+ private val repository: AlertsRepository,
+ private val colorKeeper: ColorKeeper,
+ private val selectedStudentHolder: SelectedStudentHolder,
+ private val alertCountUpdater: AlertCountUpdater
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(AlertsUiState())
+ val uiState = _uiState.asStateFlow()
+
+ private val _events = Channel()
+ val events = _events.receiveAsFlow()
+
+ private var selectedStudent: User? = null
+ private var thresholds: Map = emptyMap()
+
+ init {
+ viewModelScope.launch {
+ selectedStudentHolder.selectedStudentState.collectLatest {
+ studentChanged(it)
+ }
+ }
+ }
+
+ private suspend fun studentChanged(student: User?) {
+ if (selectedStudent != student) {
+ selectedStudent = student
+ _uiState.update {
+ it.copy(
+ studentColor = colorKeeper.getOrGenerateUserColor(student).textAndIconColor(),
+ isLoading = true
+ )
+ }
+ loadThresholds()
+ loadAlerts()
+ }
+ }
+
+ private suspend fun loadThresholds(forceNetwork: Boolean = false) {
+ selectedStudent?.let { student ->
+ val thresholds = repository.getAlertThresholdForStudent(student.id, forceNetwork)
+ this.thresholds = thresholds.associateBy { it.id }
+ }
+ }
+
+ private suspend fun loadAlerts(forceNetwork: Boolean = false) {
+ selectedStudent?.let { student ->
+ try {
+ val alerts = repository.getAlertsForStudent(student.id, forceNetwork)
+ val alertItems = alerts.map { createAlertItem(it) }
+ _uiState.update {
+ it.copy(
+ alerts = alertItems,
+ isLoading = false,
+ isError = false,
+ isRefreshing = false,
+ )
+ }
+ } catch (e: Exception) {
+ setError()
+ }
+ } ?: setError()
+
+ alertCountUpdater.updateShouldRefreshAlertCount(true)
+ }
+
+ private fun setError() {
+ _uiState.update {
+ it.copy(isLoading = false, isError = true, isRefreshing = false, alerts = emptyList())
+ }
+ }
+
+ fun handleAction(action: AlertsAction) {
+ when (action) {
+ is AlertsAction.Navigate -> {
+ viewModelScope.launch {
+ _events.send(AlertsViewModelAction.Navigate(action.route))
+ markAlertRead(action.alertId)
+ alertCountUpdater.updateShouldRefreshAlertCount(true)
+ }
+ }
+
+ is AlertsAction.Refresh -> {
+ viewModelScope.launch {
+ _uiState.update { it.copy(isRefreshing = true) }
+ loadThresholds(true)
+ loadAlerts(true)
+ }
+ }
+
+ is AlertsAction.DismissAlert -> {
+ viewModelScope.launch {
+ dismissAlert(action.alertId)
+ }
+ }
+ }
+ }
+
+ private suspend fun markAlertRead(alertId: Long) {
+ try {
+ _uiState.update { uiState ->
+ uiState.copy(
+ alerts = uiState.alerts.map { alertItem ->
+ if (alertItem.alertId == alertId) alertItem.copy(unread = false) else alertItem
+ }
+ )
+ }
+ repository.updateAlertWorkflow(alertId, AlertWorkflowState.READ)
+ alertCountUpdater.updateShouldRefreshAlertCount(true)
+ } catch (e: Exception) {
+ //No need to do anything. The alert will stay read.
+ }
+ }
+
+ private suspend fun dismissAlert(alertId: Long) {
+ fun resetAlert(alert: AlertsItemUiState) {
+ val alerts = _uiState.value.alerts.toMutableList()
+ alerts.add(alert)
+ alerts.sortByDescending { it.date }
+ _uiState.update { it.copy(alerts = alerts) }
+ viewModelScope.launch {
+ alertCountUpdater.updateShouldRefreshAlertCount(true)
+ }
+ }
+
+ val alerts = _uiState.value.alerts.toMutableList()
+ val alert = alerts.find { it.alertId == alertId } ?: return
+ alerts.removeIf { it.alertId == alertId }
+ _uiState.update { it.copy(alerts = alerts) }
+
+ try {
+ repository.updateAlertWorkflow(alertId, AlertWorkflowState.DISMISSED)
+ alertCountUpdater.updateShouldRefreshAlertCount(true)
+ _events.send(AlertsViewModelAction.ShowSnackbar(R.string.alertDismissMessage, R.string.alertDismissAction) {
+ viewModelScope.launch {
+ try {
+ repository.updateAlertWorkflow(
+ alert.alertId,
+ if (alert.unread) AlertWorkflowState.UNREAD else AlertWorkflowState.READ
+ )
+ resetAlert(alert)
+ } catch (e: Exception) {
+ _events.send(AlertsViewModelAction.ShowSnackbar(R.string.alertDismissActionErrorMessage, null, null))
+ }
+ }
+ })
+ } catch (e: Exception) {
+ _events.send(AlertsViewModelAction.ShowSnackbar(R.string.alertDismissErrorMessage, null, null))
+ resetAlert(alert)
+ }
+ }
+
+ private fun createAlertItem(alert: Alert): AlertsItemUiState {
+ return AlertsItemUiState(
+ alertId = alert.id,
+ title = alert.title,
+ alertType = alert.alertType,
+ date = alert.actionDate,
+ observerAlertThreshold = thresholds[alert.observerAlertThresholdId]?.threshold,
+ lockedForUser = alert.lockedForUser,
+ unread = alert.workflowState == AlertWorkflowState.UNREAD,
+ htmlUrl = alert.htmlUrl
+ )
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarFragment.kt
new file mode 100644
index 0000000000..9f5d6762b0
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarFragment.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.parentapp.features.calendar
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import com.instructure.pandautils.features.calendar.BaseCalendarFragment
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
+import com.instructure.parentapp.util.ParentPrefs
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class ParentCalendarFragment : BaseCalendarFragment() {
+
+ @Inject
+ lateinit var selectedStudentHolder: SelectedStudentHolder
+
+ override fun onStart() {
+ super.onStart()
+ lifecycleScope.launch {
+ selectedStudentHolder.selectedStudentChangedFlow.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collectLatest {
+ delay(100)
+ refreshCalendar()
+ }
+ }
+ }
+
+ override fun applyTheme() {
+ val student = ParentPrefs.currentStudent
+ val color = ColorKeeper.getOrGenerateUserColor(student).backgroundColor()
+ ViewStyler.setStatusBarDark(requireActivity(), color)
+ }
+
+ override fun showToolbar(): Boolean = false
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRepository.kt
new file mode 100644
index 0000000000..2a12e6151d
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRepository.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.parentapp.features.calendar
+
+import com.instructure.canvasapi2.apis.CalendarEventAPI
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.FeaturesAPI
+import com.instructure.canvasapi2.apis.PlannerAPI
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Plannable
+import com.instructure.canvasapi2.models.PlannableType
+import com.instructure.canvasapi2.models.PlannerItem
+import com.instructure.canvasapi2.models.toPlannerItems
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.canvasapi2.utils.DataResult
+import com.instructure.canvasapi2.utils.depaginate
+import com.instructure.canvasapi2.utils.toDate
+import com.instructure.pandautils.features.calendar.CalendarRepository
+import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao
+import com.instructure.pandautils.room.calendar.entities.CalendarFilterEntity
+import com.instructure.pandautils.utils.orDefault
+import com.instructure.parentapp.util.ParentPrefs
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+
+class ParentCalendarRepository(
+ private val plannerApi: PlannerAPI.PlannerInterface,
+ private val coursesApi: CourseAPI.CoursesInterface,
+ private val calendarEventApi: CalendarEventAPI.CalendarEventInterface,
+ private val apiPrefs: ApiPrefs,
+ private val featuresApi: FeaturesAPI.FeaturesInterface,
+ private val parentPrefs: ParentPrefs,
+ private val calendarFilterDao: CalendarFilterDao
+) : CalendarRepository {
+
+ private var canvasContexts: List = emptyList()
+
+ override suspend fun getPlannerItems(
+ startDate: String,
+ endDate: String,
+ contextCodes: List,
+ forceNetwork: Boolean
+ ): List {
+ if (contextCodes.isEmpty()) return emptyList()
+
+ val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork)
+
+ val allItems = coroutineScope {
+ val calendarEvents = async {
+ calendarEventApi.getCalendarEvents(
+ false,
+ CalendarEventAPI.CalendarEventType.CALENDAR.apiName,
+ startDate,
+ endDate,
+ contextCodes,
+ restParams
+ ).depaginate {
+ calendarEventApi.next(it, restParams)
+ }.dataOrThrow.toPlannerItems(PlannableType.CALENDAR_EVENT)
+ }
+
+ val calendarAssignments = async {
+ calendarEventApi.getCalendarEvents(
+ false,
+ CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName,
+ startDate,
+ endDate,
+ contextCodes,
+ restParams
+ ).depaginate {
+ calendarEventApi.next(it, restParams)
+ }.dataOrThrow.toPlannerItems(PlannableType.ASSIGNMENT)
+ }
+
+ val plannerNotes = async {
+ plannerApi.getPlannerNotes(startDate, endDate, contextCodes, restParams).depaginate {
+ plannerApi.nextPagePlannerNotes(it, restParams)
+ }.dataOrThrow.toPlannerItems()
+ }
+
+ return@coroutineScope listOf(calendarEvents, calendarAssignments, plannerNotes).awaitAll()
+ }
+
+ return allItems.flatten().sortedBy { it.plannableDate }
+ }
+
+ override suspend fun getCanvasContexts(): DataResult