From fff31115d4f04045700e587702572c99777a9a2b Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:51:15 +0200 Subject: [PATCH 01/94] Fix HelpMenuE2ETest in all the 3 apps. (#3253) Fix Report A Problem test in all the 3 apps. Add new subcategory. refs: MBL-19331 affects: Teacher, Student, Parent release note: --- .../parentapp/ui/e2e/HelpMenuE2ETest.kt | 8 ++-- .../parentapp/ui/pages/HelpPage.kt | 2 +- .../student/ui/e2e/HelpMenuE2ETest.kt | 16 +++---- .../NavigationDrawerInteractionTest.kt | 4 +- .../instructure/student/ui/pages/HelpPage.kt | 20 +++++---- .../teacher/ui/e2e/HelpMenuE2ETest.kt | 16 +++---- .../instructure/teacher/ui/pages/HelpPage.kt | 43 ++++++------------- .../canvas/espresso/TestMetaData.kt | 2 +- 8 files changed, 46 insertions(+), 65 deletions(-) diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/HelpMenuE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/HelpMenuE2ETest.kt index a7ea6fe649..5779f4e478 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/HelpMenuE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/HelpMenuE2ETest.kt @@ -21,7 +21,7 @@ import androidx.test.espresso.intent.Intents import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.checkToastText @@ -41,8 +41,7 @@ class HelpMenuE2ETest : ParentComposeTest() { @E2E @Test - @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.COMMON, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuE2E() { Log.d(PREPARATION_TAG, "Seeding data.") @@ -89,8 +88,7 @@ class HelpMenuE2ETest : ParentComposeTest() { @E2E @Test - @TestMetaData(Priority.COMMON, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.BUG_CASE, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuReportProblemE2E() { Log.d(PREPARATION_TAG, "Seeding data.") diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/HelpPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/HelpPage.kt index 523344e1df..38a3e523fb 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/HelpPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/HelpPage.kt @@ -43,7 +43,7 @@ class HelpPage : BasePage(R.id.helpDialog) { private val searchGuidesLabel by OnViewWithText(R.string.searchGuides) - private val reportProblemLabel by OnViewWithText(R.string.reportProblem) + private val reportProblemLabel by OnViewWithStringTextIgnoreCase("Report a Problem") private val submitFeatureLabel by OnViewWithStringTextIgnoreCase("Submit a Feature Idea") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/HelpMenuE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/HelpMenuE2ETest.kt index 6945cf0942..42fa7d552b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/HelpMenuE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/HelpMenuE2ETest.kt @@ -21,7 +21,7 @@ import androidx.test.espresso.intent.Intents import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.checkToastText @@ -42,8 +42,7 @@ class HelpMenuE2ETest : StudentTest() { @E2E @Test - @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.COMMON, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuE2E() { Log.d(PREPARATION_TAG, "Seeding data.") @@ -63,8 +62,8 @@ class HelpMenuE2ETest : StudentTest() { Log.d(ASSERTION_TAG, "Assert that all the corresponding Help menu content are displayed.") helpPage.assertHelpMenuContent() - Log.d(STEP_TAG, "Click on 'Report a problem' menu.") - helpPage.verifyReportAProblem("Test Subject", "Test Description") + Log.d(STEP_TAG, "Click on 'Report a Problem' menu.") + helpPage.assertReportProblemDialogDetails("Test Subject", "Test Description") Log.d(ASSERTION_TAG, "Assert that it is possible to write into the input fields and the corresponding buttons are displayed as well.") helpPage.assertReportProblemDialogDisplayed() @@ -87,8 +86,7 @@ class HelpMenuE2ETest : StudentTest() { @E2E @Test - @TestMetaData(Priority.COMMON, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.BUG_CASE, FeatureCategory.DASHBOARD, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuReportProblemE2E() { Log.d(PREPARATION_TAG, "Seeding data.") @@ -108,8 +106,8 @@ class HelpMenuE2ETest : StudentTest() { Log.d(ASSERTION_TAG, "Assert that all the corresponding Help menu content are displayed.") helpPage.assertHelpMenuContent() - Log.d(STEP_TAG, "Click on 'Report a problem' menu.") - helpPage.verifyReportAProblem("Test Subject", "Test Description") + Log.d(STEP_TAG, "Click on 'Report a Problem' menu.") + helpPage.assertReportProblemDialogDetails("Test Subject", "Test Description") Log.d(ASSERTION_TAG, "Assert that it is possible to write into the input fields and the corresponding buttons are displayed as well.") helpPage.assertReportProblemDialogDisplayed() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt index f0d9aac9f9..9b370a82ed 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt @@ -146,7 +146,7 @@ class NavigationDrawerInteractionTest : StudentTest() { signInStudent() leftSideNavigationDrawerPage.clickHelpMenu() - helpPage.verifyAskAQuestion(course, "Here's a question") + helpPage.assertAskYourInstructorDialogDetails(course, "Here's a question") } // Should open the Canvas guides in a WebView @@ -169,7 +169,7 @@ class NavigationDrawerInteractionTest : StudentTest() { signInStudent() leftSideNavigationDrawerPage.clickHelpMenu() - helpPage.verifyReportAProblem("Problem", "It's a problem!") + helpPage.assertReportProblemDialogDetails("Problem", "It's a problem!") } // Should send an intent to open the listing for Student App in the Play Store diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt index cfe0f2c22b..60b06ee29d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt @@ -43,17 +43,15 @@ import com.instructure.espresso.typeText import com.instructure.student.R import org.hamcrest.CoreMatchers -// This is a little hokey, as the options that appear are somewhat governed by the results of -// the /api/v1/accounts/self/help_links call. If that changes a lot over time (thus breaking -// this test), we can back off to some easier test like "some options are visible". class HelpPage : BasePage(R.id.helpDialog) { + private val askInstructorLabel by OnViewWithText(R.string.askInstructor) private val searchGuidesLabel by OnViewWithText(R.string.searchGuides) - private val reportProblemLabel by OnViewWithStringTextIgnoreCase("Report a problem") + private val reportProblemLabel by OnViewWithStringTextIgnoreCase("Report a Problem") private val submitFeatureLabel by OnViewWithStringTextIgnoreCase("Submit a Feature Idea") private val shareLoveLabel by OnViewWithText(R.string.shareYourLove) - fun verifyAskAQuestion(course: Course, question: String) { + fun assertAskYourInstructorDialogDetails(course: Course, question: String) { askInstructorLabel.scrollTo().click() waitForView(withText(course.name)).assertDisplayed() // Verify that our course is selected in the spinner onView(withId(R.id.message)).scrollTo().perform(withCustomConstraints(typeText(question), isDisplayingAtLeast(1))) @@ -62,7 +60,7 @@ class HelpPage : BasePage(R.id.helpDialog) { onView(containsTextCaseInsensitive("Send")).assertDisplayed() } - fun verifyAskAQuestion(course: CourseApiModel, question: String) { + private fun assertAskYourInstructorDialogDetails(course: CourseApiModel, question: String) { askInstructorLabel.scrollTo().click() waitForView(withText(course.name)).assertDisplayed() // Verify that our course is selected in the spinner onView(withId(R.id.message)).scrollTo().perform(withCustomConstraints(typeText(question), isDisplayingAtLeast(1))) @@ -72,7 +70,7 @@ class HelpPage : BasePage(R.id.helpDialog) { } fun sendQuestionToInstructor(course: CourseApiModel, question: String) { - verifyAskAQuestion(course, question) + assertAskYourInstructorDialogDetails(course, question) onView(containsTextCaseInsensitive("Send")).click() } @@ -80,14 +78,14 @@ class HelpPage : BasePage(R.id.helpDialog) { searchGuidesLabel.scrollTo().click() } - fun verifyReportAProblem(subject: String, description: String) { + fun assertReportProblemDialogDetails(subject: String, description: String) { reportProblemLabel.scrollTo().click() onView(withId(R.id.subjectEditText)).typeText(subject) Espresso.closeSoftKeyboard() onView(withId(R.id.descriptionEditText)).typeText(description) Espresso.closeSoftKeyboard() // Let's just make sure that the "Send" button is displayed, rather than actually pressing it - onView(containsTextCaseInsensitive("Send")).scrollTo().assertDisplayed() + assertSendReportProblemButtonDisplayed() } fun assertReportProblemDialogDisplayed() { @@ -102,6 +100,10 @@ class HelpPage : BasePage(R.id.helpDialog) { onView(containsTextCaseInsensitive("Send")).scrollTo().click() } + private fun assertSendReportProblemButtonDisplayed() { + onView(containsTextCaseInsensitive("Send")).scrollTo().assertDisplayed() + } + fun clickShareLoveLabel() { shareLoveLabel.scrollTo().click() } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/HelpMenuE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/HelpMenuE2ETest.kt index 7a7831a340..7e0c77d58b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/HelpMenuE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/HelpMenuE2ETest.kt @@ -21,7 +21,7 @@ import androidx.test.espresso.intent.Intents import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.checkToastText @@ -42,8 +42,7 @@ class HelpMenuE2ETest: TeacherTest() { @E2E @Test - @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.COMMON, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuE2E() { Log.d(PREPARATION_TAG, "Seeding data.") @@ -63,8 +62,8 @@ class HelpMenuE2ETest: TeacherTest() { Log.d(ASSERTION_TAG, "Assert that all the corresponding Help menu content are displayed.") helpPage.assertHelpMenuContent() - Log.d(STEP_TAG, "Click on 'Report a problem' menu.") - helpPage.verifyReportAProblem("Test Subject", "Test Description") + Log.d(STEP_TAG, "Click on 'Report a Problem' menu.") + helpPage.assertReportProblemDialogDetails("Test Subject", "Test Description") Log.d(ASSERTION_TAG, "Assert that it is possible to write into the input fields and the corresponding buttons are displayed as well.") helpPage.assertReportProblemDialogDisplayed() @@ -89,8 +88,7 @@ class HelpMenuE2ETest: TeacherTest() { @E2E @Test - @TestMetaData(Priority.COMMON, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.BUG_CASE, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuReportProblemE2E() { Log.d(PREPARATION_TAG, "Seeding data.") @@ -110,8 +108,8 @@ class HelpMenuE2ETest: TeacherTest() { Log.d(ASSERTION_TAG, "Assert that all the corresponding Help menu content are displayed.") helpPage.assertHelpMenuContent() - Log.d(STEP_TAG, "Click on 'Report a problem' menu.") - helpPage.verifyReportAProblem("Test Subject", "Test Description") + Log.d(STEP_TAG, "Click on 'Report a Problem' menu.") + helpPage.assertReportProblemDialogDetails("Test Subject", "Test Description") Log.d(ASSERTION_TAG, "Assert that it is possible to write into the input fields and the corresponding buttons are displayed as well.") helpPage.assertReportProblemDialogDisplayed() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/HelpPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/HelpPage.kt index 979bbd0f3b..005ff36fb8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/HelpPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/HelpPage.kt @@ -19,20 +19,15 @@ package com.instructure.teacher.ui.pages import android.app.Instrumentation import android.content.Intent import androidx.test.espresso.Espresso -import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.IntentMatchers -import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.withCustomConstraints -import com.instructure.canvasapi2.models.Course import com.instructure.espresso.OnViewWithStringTextIgnoreCase import com.instructure.espresso.OnViewWithText import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click -import com.instructure.espresso.matchers.WaitForViewMatcher.waitForView import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView import com.instructure.espresso.page.plus @@ -43,16 +38,11 @@ import com.instructure.teacher.R import org.hamcrest.CoreMatchers /** - * A page representing the Help menu in the application. + * A page representing the Help menu in the Teacher application. * */ class HelpPage : BasePage(R.id.helpDialog) { - /** - * The label for asking an instructor. - */ - private val askInstructorLabel by OnViewWithText(R.string.askInstructor) - /** * The label for searching guides. */ @@ -61,7 +51,7 @@ class HelpPage : BasePage(R.id.helpDialog) { /** * The label for reporting a problem. */ - private val reportProblemLabel by OnViewWithText(R.string.reportProblem) + private val reportProblemLabel by OnViewWithText(R.string.report_problem) /** * The label for submitting a feature idea. @@ -73,34 +63,20 @@ class HelpPage : BasePage(R.id.helpDialog) { */ private val shareLoveLabel by OnViewWithText(R.string.shareYourLove) - /** - * Verifies asking a question to an instructor. - * - * @param course The course to select in the spinner. - * @param question The question to type in the message field. - */ - fun verifyAskAQuestion(course: Course, question: String) { - askInstructorLabel.scrollTo().click() - waitForView(withText(course.name)).assertDisplayed() - onView(withId(R.id.message)).scrollTo().perform(withCustomConstraints(typeText(question), isDisplayingAtLeast(1))) - Espresso.closeSoftKeyboard() - onView(containsTextCaseInsensitive("Send")).assertDisplayed() - } - /** * Clicks on the 'Search the Canvas Guides' help menu. */ - fun clickSearchGuidesLabel() { + private fun clickSearchGuidesLabel() { searchGuidesLabel.scrollTo().click() } /** - * Verifies reporting a problem. + * Asserts the details of the 'Report a problem' dialog by filling in the subject and description fields and checking for the 'Send' button. * * @param subject The subject of the problem. * @param description The description of the problem. */ - fun verifyReportAProblem(subject: String, description: String) { + fun assertReportProblemDialogDetails(subject: String, description: String) { reportProblemLabel.scrollTo().click() onView(withId(R.id.subjectEditText)).typeText(subject) Espresso.closeSoftKeyboard() @@ -109,6 +85,9 @@ class HelpPage : BasePage(R.id.helpDialog) { onView(containsTextCaseInsensitive("Send")).scrollTo().assertDisplayed() } + /** + * Clicks on the 'Send' button on the 'Report a problem' dialog. + */ fun clickSendReportProblem() { onView(containsTextCaseInsensitive("Send")).scrollTo().click() } @@ -200,6 +179,12 @@ class HelpPage : BasePage(R.id.helpDialog) { onView(withId(R.id.subtitle) + withText("Tell us about your favorite parts of the app")).assertDisplayed() } + /** + * Asserts that clicking on a Help menu item launches an intent with the expected URL. + * + * @param helpMenuText The text of the Help menu item to click. + * @param expectedURL The expected URL that should be opened. + */ fun assertHelpMenuURL(helpMenuText: String, expectedURL: String) { val expectedIntent = CoreMatchers.allOf( IntentMatchers.hasAction(Intent.ACTION_VIEW), diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt index 397bb72747..83b57e31a7 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt @@ -44,7 +44,7 @@ enum class SecondaryFeatureCategory { GROUPS_DASHBOARD, GROUPS_FILES, GROUPS_ANNOUNCEMENTS, GROUPS_DISCUSSIONS, GROUPS_PAGES, GROUPS_PEOPLE, EVENTS_DISCUSSIONS, EVENTS_QUIZZES, EVENTS_ASSIGNMENTS, EVENTS_NOTIFICATIONS, SETTINGS_EMAIL_NOTIFICATIONS, MODULES_ASSIGNMENTS, MODULES_DISCUSSIONS, MODULES_FILES, MODULES_PAGES, MODULES_QUIZZES, OFFLINE_MODE, ALL_COURSES, CHANGE_USER, ASSIGNMENT_REMINDER, ASSIGNMENT_DETAILS, SMART_SEARCH, ADD_STUDENT, - SYLLABUS, SUMMARY, FRONT_PAGE, CANVAS_NETWORK, INBOX_SIGNATURE, ACCESS_TOKEN_EXPIRATION, SECTIONS + SYLLABUS, SUMMARY, FRONT_PAGE, CANVAS_NETWORK, INBOX_SIGNATURE, ACCESS_TOKEN_EXPIRATION, SECTIONS, HELP_MENU } enum class TestCategory { From 4359561f426a0bc7d8fe67c7e4bd8142bffe4549 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:13:23 +0200 Subject: [PATCH 02/94] [MBL-19109][Student] Consortia quizzes open with root account (#3260) refs: MBL-19109 affects: Student release note: Fixed a bug where quizzes would not open in some cases. test plan: See ticket --- .../student/fragment/BasicQuizViewFragment.kt | 27 ++++++++++++++++--- .../canvasapi2/managers/OAuthManager.kt | 4 +-- .../canvasapi2/managers/QuizManager.kt | 9 +++---- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt index 5cc0d9036f..fdbea2334b 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt @@ -57,7 +57,7 @@ class BasicQuizViewFragment : InternalWebviewFragment() { private var apiURL: String? by NullableStringArg() private var quiz: Quiz? by NullableParcelableArg() @get:PageViewUrlParam("quizId") - var quizId: Long by LongArg() + var quizId: Long by LongArg(key = RouterParams.QUIZ_ID) private var isTakingQuiz = false override fun title(): String = getString(R.string.quizzes) @@ -210,11 +210,29 @@ class BasicQuizViewFragment : InternalWebviewFragment() { private suspend fun processQuizDetails(url: String) { // Only show the lock if submissions are empty, otherwise let them view their submission - if (quiz?.lockInfo != null && awaitApi { QuizManager.getFirstPageQuizSubmissions(canvasContext, quiz!!.id, true, it) }.quizSubmissions.isEmpty()) { - populateWebView(LockInfoHTMLHelper.getLockedInfoHTML(quiz?.lockInfo!!, requireContext(), R.string.lockedQuizDesc)) + if (quiz?.lockInfo != null && awaitApi { + QuizManager.getFirstPageQuizSubmissions( + canvasContext, + quiz!!.id, + true, + it + ) + }.quizSubmissions.isEmpty()) { + populateWebView( + LockInfoHTMLHelper.getLockedInfoHTML( + quiz?.lockInfo!!, + requireContext(), + R.string.lockedQuizDesc + ) + ) } else { val authenticatedUrl = tryOrNull { - awaitApi { OAuthManager.getAuthenticatedSession(url, it) }.sessionUrl + awaitApi { + OAuthManager.getAuthenticatedSession( + url, it, + ApiPrefs.overrideDomains[canvasContext.id] + ) + }.sessionUrl } getCanvasWebView()?.loadUrl(authenticatedUrl ?: url, APIHelper.referrer) } @@ -276,6 +294,7 @@ class BasicQuizViewFragment : InternalWebviewFragment() { Bundle().apply { putString(Const.URL, url) putParcelable(Const.QUIZ, quiz) + putLong(RouterParams.QUIZ_ID, quiz.id) })) } } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/OAuthManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/OAuthManager.kt index c14f354dee..e3bc6ca538 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/OAuthManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/OAuthManager.kt @@ -50,9 +50,9 @@ object OAuthManager { fun getAuthenticatedSessionAsync(targetUrl: String) = apiAsync { getAuthenticatedSession(targetUrl, it) } - fun getAuthenticatedSession(targetUrl: String, callback: StatusCallback) { + fun getAuthenticatedSession(targetUrl: String, callback: StatusCallback, domain: String? = null) { val adapter = RestBuilder(callback) - val params = RestParams(isForceReadFromNetwork = true) + val params = RestParams(isForceReadFromNetwork = true, domain = domain) Logger.d("targetURL to be authed: $targetUrl") val userId = ApiPrefs.user?.id if (ApiPrefs.isMasquerading && userId != null) { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/QuizManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/QuizManager.kt index e7e5874d4c..f88e4e3e15 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/QuizManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/QuizManager.kt @@ -20,15 +20,14 @@ import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.apis.QuizAPI import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.models.QuizSubmissionResponse import com.instructure.canvasapi2.models.postmodels.QuizPostBody import com.instructure.canvasapi2.models.postmodels.QuizPostBodyWrapper import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.ExhaustiveListCallback import com.instructure.canvasapi2.utils.weave.apiAsync -import com.instructure.canvasapi2.utils.weave.awaitApi -import okhttp3.ResponseBody -import java.util.* object QuizManager { @@ -88,7 +87,7 @@ object QuizManager { callback: StatusCallback ) { val adapter = RestBuilder(callback) - val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork, domain = ApiPrefs.overrideDomains[canvasContext.id]) QuizAPI.getDetailedQuiz(canvasContext, quizId, adapter, params, callback) } From 83949e698d06f3e1da34fce9fa5af3943f740df2 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Mon, 29 Sep 2025 08:59:25 +0200 Subject: [PATCH 03/94] [MBL-19358][Teacher] Scrolling in New Quiz submissions is not smooth refs: MBL-19358 affects: Teacher release note: Fixed a scrolling issue in SpeedGrader when grading New Quizzes assignments. --- .../speedgrader/content/SpeedGraderContentScreen.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentScreen.kt index 628b1ef4ef..83f0d4e992 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentScreen.kt @@ -157,7 +157,11 @@ private fun SpeedGraderContentScreen( arguments = route.bundle, modifier = Modifier .fillMaxSize() - .conditional(content is PdfContent || content is DiscussionContent) { + .conditional( + content is PdfContent || + content is DiscussionContent || + content is ExternalToolContent + ) { pointerInput(Unit) { awaitPointerEventScope { while (true) { From 7c4a91b2cfad388648d0163224f829d05a935154 Mon Sep 17 00:00:00 2001 From: domonkosadam <53480952+domonkosadam@users.noreply.github.com> Date: Mon, 29 Sep 2025 09:18:10 +0200 Subject: [PATCH 04/94] [CLX-2876][Horizon] Notifications screen (#3252) refs: CLX-2876 affects: Horizon release note: none --- .../canvasapi2/apis/UnreadCountAPI.kt | 3 + .../features/dashboard/DashboardRepository.kt | 7 + .../features/dashboard/DashboardScreen.kt | 23 +- .../features/dashboard/DashboardUiState.kt | 6 + .../features/dashboard/DashboardViewModel.kt | 15 + .../navigation/HorizonInboxNavigation.kt | 2 +- .../notification/NotificationRepository.kt | 33 +- .../notification/NotificationScreen.kt | 289 ++++++++++++++---- .../notification/NotificationUiState.kt | 27 +- .../notification/NotificationViewModel.kt | 146 ++++----- .../notification/StreamItemExtensions.kt | 35 +++ .../horizonui/foundation/HorizonColors.kt | 4 + .../horizon/horizonui/molecules/StatusChip.kt | 30 +- .../organisms/scaffolds/HorizonScaffold.kt | 2 + libs/horizon/src/main/res/values/strings.xml | 10 +- .../pandautils/utils/DateExtensions.kt | 8 + 16 files changed, 475 insertions(+), 165 deletions(-) create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/notification/StreamItemExtensions.kt diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt index 39df056dd7..9d6e3dba91 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt @@ -24,6 +24,9 @@ object UnreadCountAPI { @GET("users/self/activity_stream/summary?only_active_courses=true") fun getNotificationsCount(): Call> + @GET("users/self/activity_stream/summary?only_active_courses=true") + suspend fun getNotificationsCount(@Tag params: RestParams): DataResult> + @GET("users/self/observer_alerts/unread_count") fun getUnreadAlertCount(@Query("student_id") studentId: Long): Call diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt index 7e9037d5d9..172216885a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt @@ -17,6 +17,7 @@ package com.instructure.horizon.features.dashboard import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.apis.UnreadCountAPI import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.managers.DashboardContent import com.instructure.canvasapi2.managers.HorizonGetCoursesManager @@ -24,6 +25,7 @@ import com.instructure.canvasapi2.managers.graphql.JourneyApiManager import com.instructure.canvasapi2.managers.graphql.Program import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.UnreadNotificationCount import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import javax.inject.Inject @@ -34,6 +36,7 @@ class DashboardRepository @Inject constructor( private val apiPrefs: ApiPrefs, private val enrollmentApi: EnrollmentAPI.EnrollmentInterface, private val journeyApiManager: JourneyApiManager, + private val unreadCountApi: UnreadCountAPI.UnreadCountsInterface, ) { suspend fun getDashboardContent(forceNetwork: Boolean): DataResult { return horizonGetCoursesManager.getDashboardContent(apiPrefs.user?.id ?: -1, forceNetwork) @@ -56,4 +59,8 @@ class DashboardRepository @Inject constructor( suspend fun getPrograms(forceNetwork: Boolean = false): List { return journeyApiManager.getPrograms(forceNetwork) } + + suspend fun getUnreadCounts(forceNetwork: Boolean = true): List { + return unreadCountApi.getNotificationsCount(RestParams(isForceReadFromNetwork = forceNetwork)).dataOrNull.orEmpty() + } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt index eaab21d171..5401bd651f 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt @@ -81,6 +81,9 @@ import com.instructure.horizon.horizonui.foundation.HorizonElevation import com.instructure.horizon.horizonui.foundation.HorizonSpace import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.foundation.SpaceSize +import com.instructure.horizon.horizonui.molecules.Badge +import com.instructure.horizon.horizonui.molecules.BadgeContent +import com.instructure.horizon.horizonui.molecules.BadgeType import com.instructure.horizon.horizonui.molecules.Button import com.instructure.horizon.horizonui.molecules.ButtonColor import com.instructure.horizon.horizonui.molecules.IconButton @@ -234,14 +237,30 @@ private fun HomeScreenTopBar(uiState: DashboardUiState, mainNavController: NavCo mainNavController.navigate(MainNavigationRoute.Notification.route) }, elevation = HorizonElevation.level4, - color = IconButtonColor.Inverse + color = IconButtonColor.Inverse, + badge = if (uiState.unreadCountState.unreadNotifications > 0) { + { + Badge( + content = BadgeContent.Color, + type = BadgeType.Inverse + ) + } + } else null ) HorizonSpace(SpaceSize.SPACE_8) IconButton( iconRes = R.drawable.mail, onClick = { mainNavController.navigate(MainNavigationRoute.Inbox.route) }, elevation = HorizonElevation.level4, - color = IconButtonColor.Inverse + color = IconButtonColor.Inverse, + badge = if (uiState.unreadCountState.unreadConversations > 0) { + { + Badge( + content = BadgeContent.Color, + type = BadgeType.Inverse + ) + } + } else null ) } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt index 955f560071..6fc5a4fc90 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt @@ -25,6 +25,7 @@ data class DashboardUiState( val coursesUiState: List = emptyList(), val invitesUiState: List = emptyList(), val loadingState: LoadingState = LoadingState(), + val unreadCountState: DashboardUnreadState = DashboardUnreadState(), ) data class DashboardCourseUiState( @@ -58,4 +59,9 @@ data class CourseInviteUiState( data class DashboardProgramUiState( val id: String, val name: String, +) + +data class DashboardUnreadState( + val unreadConversations: Int = 0, + val unreadNotifications: Int = 0, ) \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt index 538adcfc9b..a61202f4e8 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt @@ -110,6 +110,7 @@ class DashboardViewModel @Inject constructor( dismissInvite(courseInvite.courseId) }) } + loadUnreadCount() _uiState.update { it.copy( programsUiState = programsUiState, @@ -258,4 +259,18 @@ class DashboardViewModel @Inject constructor( currentState.copy(invitesUiState = updatedInvites) } } + + private suspend fun loadUnreadCount() { + val unreadCounts = dashboardRepository.getUnreadCounts(true) + val unreadConversations = unreadCounts.firstOrNull { it.type == "Conversation" }?.count ?: 0 + val unreadNotifications = unreadCounts.filter { it.type == "Message" }.sumOf { it.unreadCount } + _uiState.update { + it.copy( + unreadCountState = DashboardUnreadState( + unreadConversations = unreadConversations, + unreadNotifications = unreadNotifications + ) + ) + } + } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/navigation/HorizonInboxNavigation.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/navigation/HorizonInboxNavigation.kt index e140b26e95..71a05eee05 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/navigation/HorizonInboxNavigation.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/navigation/HorizonInboxNavigation.kt @@ -160,7 +160,7 @@ fun NavGraphBuilder.horizonInboxNavigation( val courseId = backStackEntry.arguments?.getLong(HorizonInboxRoute.InboxDetails.COURSE_ID) ?: return@LaunchedEffect val id = backStackEntry.arguments?.getLong(HorizonInboxRoute.InboxDetails.ID) ?: return@LaunchedEffect navController.navigate(HorizonInboxRoute.InboxDetails.route(id, HorizonInboxItemType.CourseNotification, courseId)) { - popUpTo(HorizonInboxRoute.InboxDetailsDeepLink.route) { + popUpTo(HorizonInboxRoute.CourseAnnouncementDetailsDeepLink.route) { inclusive = true } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationRepository.kt index 4b8ebc923a..6ffad4cc11 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationRepository.kt @@ -16,27 +16,58 @@ */ package com.instructure.horizon.features.notification +import com.instructure.canvasapi2.apis.AccountNotificationAPI import com.instructure.canvasapi2.apis.CourseAPI import com.instructure.canvasapi2.apis.StreamAPI import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.models.AccountNotification import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.StreamItem +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.depaginate import javax.inject.Inject class NotificationRepository @Inject constructor( + private val apiPrefs: ApiPrefs, private val streamApi: StreamAPI.StreamInterface, - private val courseApi: CourseAPI.CoursesInterface + private val courseApi: CourseAPI.CoursesInterface, + private val accountNotificationApi: AccountNotificationAPI.AccountNotificationInterface, + private val getCoursesManager: HorizonGetCoursesManager ) { suspend fun getNotifications(forceRefresh: Boolean): List { + val courseIds = getCoursesInProgress(forceRefresh) val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceRefresh) return streamApi.getUserStream(restParams) .depaginate { streamApi.getNextPageStream(it, restParams) } .dataOrThrow + .filter { + it.courseId == -1L || courseIds.contains(it.courseId) + } + .filter { + it.isCourseNotification() + || it.isDueDateNotification() + || it.isNotificationItemScored() + || it.isGradingPeriodNotification() + } + } + + suspend fun getGlobalAnnouncements(forceRefresh: Boolean): List { + val params = RestParams(isForceReadFromNetwork = forceRefresh, usePerPageQueryParam = true) + return accountNotificationApi.getAccountNotifications(params, true, true) + .depaginate { accountNotificationApi.getNextPageNotifications(it, params) } + .dataOrThrow } suspend fun getCourse(courseId: Long): Course { val restParams = RestParams() return courseApi.getCourse(courseId, restParams).dataOrThrow } + + private suspend fun getCoursesInProgress(forceRefresh: Boolean): List { + return getCoursesManager + .getCoursesWithProgress(apiPrefs.user?.id ?: -1L, forceRefresh) + .dataOrThrow + .map { it.courseId } + } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt index e72198b7e0..90f2293cbc 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt @@ -16,136 +16,210 @@ */ package com.instructure.horizon.features.notification +import android.content.Context import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import androidx.navigation.NavDeepLinkRequest +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R +import com.instructure.horizon.horizonui.foundation.HorizonBorder import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius import com.instructure.horizon.horizonui.foundation.HorizonSpace import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.foundation.SpaceSize -import com.instructure.horizon.horizonui.molecules.HorizonDivider -import com.instructure.horizon.horizonui.molecules.IconButton -import com.instructure.horizon.horizonui.molecules.IconButtonColor +import com.instructure.horizon.horizonui.molecules.Badge +import com.instructure.horizon.horizonui.molecules.BadgeContent +import com.instructure.horizon.horizonui.molecules.BadgeType +import com.instructure.horizon.horizonui.molecules.StatusChip +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import com.instructure.horizon.horizonui.molecules.StatusChipState import com.instructure.horizon.horizonui.organisms.scaffolds.HorizonScaffold +import com.instructure.horizon.horizonui.platform.LoadingState import com.instructure.horizon.horizonui.platform.LoadingStateWrapper +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.getActivityOrNull +import com.instructure.pandautils.utils.isPreviousDay +import com.instructure.pandautils.utils.isSameDay +import com.instructure.pandautils.utils.isSameWeek +import com.instructure.pandautils.utils.localisedFormat +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable -fun NotificationScreen(state: NotificationUiState, mainNavController: NavController) { +fun NotificationScreen(state: NotificationUiState, mainNavController: NavHostController) { + val activity = LocalContext.current.getActivityOrNull() + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + LaunchedEffect(Unit) { + if (activity != null) ViewStyler.setStatusBarColor(activity, ContextCompat.getColor(activity, R.color.surface_pagePrimary)) + } + HorizonScaffold( title = stringResource(R.string.notificationsTitle), + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, onBackPressed = { mainNavController.popBackStack() }, ) { modifier -> LoadingStateWrapper(state.screenState) { - NotificationContent(state, modifier) + NotificationContent( + mainNavController, + state, + showSnackbar = { message -> + scope.launch { snackbarHostState.showSnackbar(message) } + }, + modifier + ) } } } @Composable -private fun NotificationContent(state: NotificationUiState, modifier: Modifier = Modifier) { +private fun NotificationContent( + navController: NavHostController, + state: NotificationUiState, + showSnackbar: (String) -> Unit, + modifier: Modifier = Modifier +) { Column( modifier = modifier.background(HorizonColors.Surface.pageSecondary()) ) { LazyColumn( - contentPadding = PaddingValues(top = 16.dp), + contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp), modifier = Modifier .weight(1f) ) { - if (state.allNotificationItems.isEmpty()) { + if (state.notificationItems.isEmpty()) { item { EmptyNotificationItemContent() } } else { - items(state.pagedNotificationItems[state.currentPageIndex]) { item -> - Column { - NotificationItemContent( - categoryLabel = item.categoryLabel, - title = item.title, - date = item.date - ) - - HorizonDivider() - } + items(state.notificationItems) { item -> + NotificationItemContent(navController, item, showSnackbar) } } } - - HorizonDivider() - - if (state.pagedNotificationItems.size > 1) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - ) { - IconButton( - iconRes = R.drawable.chevron_left, - onClick = state.decreasePageIndex, - color = IconButtonColor.Black, - enabled = state.currentPageIndex > 0 && state.pagedNotificationItems.size > 1 - ) - - HorizonSpace(SpaceSize.SPACE_8) - - IconButton( - iconRes = R.drawable.chevron_right, - onClick = state.increasePageIndex, - color = IconButtonColor.Black, - enabled = state.currentPageIndex < state.pagedNotificationItems.lastIndex - ) - } - } } } @Composable private fun EmptyNotificationItemContent() { - NotificationItemContent( - categoryLabel = "", - title = stringResource(R.string.notificationsEmptyMessage), - date = "" + Text( + stringResource(R.string.notificationsEmptyMessage), + style = HorizonTypography.p1, + color = HorizonColors.Text.body(), + modifier = Modifier + .padding(horizontal = 24.dp, vertical = 8.dp) ) } @Composable private fun NotificationItemContent( - categoryLabel: String, - title: String, - date: String, + navController: NavHostController, + notificationItem: NotificationItem, + showSnackbar: (String) -> Unit ) { + val context = LocalContext.current Column( modifier = Modifier - .padding(horizontal = 24.dp, vertical = 16.dp) + .padding(horizontal = 24.dp, vertical = 8.dp) + .border( + HorizonBorder.level2(HorizonColors.LineAndBorder.lineStroke()), + HorizonCornerRadius.level2 + ) + .clip(HorizonCornerRadius.level2) + .fillMaxWidth() + .clickable { + when (val route = notificationItem.route) { + is NotificationRoute.DeepLink -> { + val request = NavDeepLinkRequest.Builder + .fromUri(route.deepLink.toUri()) + .build() + + try { + navController.navigate(request) + } catch (e: IllegalArgumentException) { + showSnackbar(context.getString(R.string.notificationsFailedToOpenMessage)) + } + } + + is NotificationRoute.ExplicitRoute -> { + navController.navigate(route.route) + } + } + } + .padding(horizontal = 16.dp) ) { - Text( - text = categoryLabel, - style = HorizonTypography.labelSmallBold, - color = HorizonColors.Text.timestamp() - ) + HorizonSpace(SpaceSize.SPACE_16) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + StatusChip( + state = StatusChipState( + label = notificationItem.category.label, + color = notificationItem.category.color, + fill = true + ) + ) - HorizonSpace(SpaceSize.SPACE_4) + Spacer(modifier = Modifier.weight(1f)) + + if (!notificationItem.isRead){ + Badge( + content = BadgeContent.ColorSmall, + type = BadgeType.Custom( + backgroundColor = HorizonColors.Surface.inversePrimary(), + contentColor = HorizonColors.Surface.institution() + ) + ) + } + } + HorizonSpace(SpaceSize.SPACE_8) + + if (notificationItem.courseLabel != null) { + Text( + text = notificationItem.courseLabel, + style = HorizonTypography.p3, + color = HorizonColors.Text.timestamp() + ) + HorizonSpace(SpaceSize.SPACE_8) + } Text( - text = title, + text = notificationItem.title, style = HorizonTypography.p1, color = HorizonColors.Text.body(), maxLines = 2, @@ -155,9 +229,94 @@ private fun NotificationItemContent( HorizonSpace(SpaceSize.SPACE_4) Text( - text = date, - style = HorizonTypography.labelSmall, + text = notificationItem.date.toLocalisedFormat(LocalContext.current), + style = HorizonTypography.p3, color = HorizonColors.Text.timestamp() ) + HorizonSpace(SpaceSize.SPACE_16) + } +} + +private fun Date?.toLocalisedFormat(context: Context): String { + if (this == null) return "" + if (this.isSameDay(Date())) { + return context.getString(R.string.notificationsDateToday) } + if (this.isPreviousDay(Date())) { + return context.getString(R.string.notificationsDateYesterday) + } + if (this.isSameWeek(Date())) { + return SimpleDateFormat("EEEE", Locale.getDefault()).format(this) + } + return this.localisedFormat("MMM dd, yyyy") +} + +@Composable +@Preview +private fun NotificationsScreenPreview() { + ContextKeeper.appContext = LocalContext.current + val sampleNotifications = listOf( + NotificationItem( + category = NotificationItemCategory( + stringResource(R.string.notificationsDueDateCategoryLabel), + StatusChipColor.Honey + ), + title = "Your assignment is due soon", + courseLabel = "Biology 101", + date = Date(), + isRead = false, + route = NotificationRoute.DeepLink("myapp://course/1/assignment/1") + ), + NotificationItem( + category = NotificationItemCategory( + stringResource(R.string.notificationsScoreChangedCategoryLabel), + StatusChipColor.Violet + ), + title = "Your score has been updated", + courseLabel = "Math 201", + date = Calendar.getInstance().apply { time = Date(); add(Calendar.DAY_OF_WEEK, -1) }.time, + isRead = true, + route = NotificationRoute.ExplicitRoute("course/2/grade") + ), + NotificationItem( + category = NotificationItemCategory( + stringResource(R.string.notificationsAnnouncementCategoryLabel), + StatusChipColor.Sky + ), + title = "New announcement in your course", + courseLabel = null, + date = Calendar.getInstance().apply { time = Date(); add(Calendar.DAY_OF_WEEK, -2) }.time, + isRead = false, + route = NotificationRoute.DeepLink("myapp://course/3/announcement/1") + ), + NotificationItem( + category = NotificationItemCategory( + stringResource(R.string.notificationsAnnouncementCategoryLabel), + StatusChipColor.Sky + ), + title = "New announcement in your course", + courseLabel = null, + date = Calendar.getInstance().apply { time = Date(); add(Calendar.DAY_OF_WEEK, -6) }.time, + isRead = false, + route = NotificationRoute.DeepLink("myapp://course/3/announcement/1") + ), + NotificationItem( + category = NotificationItemCategory( + stringResource(R.string.notificationsAnnouncementCategoryLabel), + StatusChipColor.Sky + ), + title = "New announcement in your course", + courseLabel = null, + date = Calendar.getInstance().apply { time = Date(); add(Calendar.DAY_OF_WEEK, -7) }.time, + isRead = false, + route = NotificationRoute.DeepLink("myapp://course/3/announcement/1") + ) + ) + + val previewState = NotificationUiState( + screenState = LoadingState(), + notificationItems = sampleNotifications + ) + + NotificationContent(navController = rememberNavController(), state = previewState, {}) } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationUiState.kt index 4091298e7c..3316a09aaf 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationUiState.kt @@ -1,18 +1,29 @@ package com.instructure.horizon.features.notification +import com.instructure.horizon.horizonui.molecules.StatusChipColor import com.instructure.horizon.horizonui.platform.LoadingState +import java.util.Date data class NotificationUiState( val screenState: LoadingState, - val allNotificationItems: List = emptyList(), - val pagedNotificationItems: List> = emptyList(), - val currentPageIndex: Int = 0, - val decreasePageIndex: () -> Unit = {}, - val increasePageIndex: () -> Unit = {}, + val notificationItems: List = emptyList(), ) data class NotificationItem( - val categoryLabel: String, + val category: NotificationItemCategory, + val courseLabel: String?, val title: String, - val date: String, -) \ No newline at end of file + val date: Date?, + val isRead: Boolean, + val route: NotificationRoute, +) + +data class NotificationItemCategory( + val label: String, + val color: StatusChipColor, +) + +sealed class NotificationRoute { + data class DeepLink(val deepLink: String): NotificationRoute() + data class ExplicitRoute(val route: String): NotificationRoute() +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationViewModel.kt index a626ac06ee..c739d4bb17 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationViewModel.kt @@ -19,14 +19,14 @@ package com.instructure.horizon.features.notification import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.StreamItem import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.R +import com.instructure.horizon.features.inbox.HorizonInboxItemType +import com.instructure.horizon.features.inbox.navigation.HorizonInboxRoute +import com.instructure.horizon.horizonui.molecules.StatusChipColor import com.instructure.horizon.horizonui.platform.LoadingState -import com.instructure.pandautils.utils.localisedFormat -import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow @@ -45,8 +45,6 @@ class NotificationViewModel @Inject constructor( onRefresh = ::refresh, onSnackbarDismiss = ::dismissSnackbar ), - decreasePageIndex = ::decreasePageIndex, - increasePageIndex = ::increasePageIndex, ) ) val uiState = _uiState.asStateFlow() @@ -74,18 +72,43 @@ class NotificationViewModel @Inject constructor( } private suspend fun loadData(forceRefresh: Boolean = false) { - val notifications = repository.getNotifications(forceRefresh) - val items = notifications.map { + val userNotifications = repository.getNotifications(forceRefresh).map { NotificationItem( - categoryLabel = getNotificationItemCategoryLabel(it), + category = getNotificationItemCategoryLabel(it), title = getNotificationItemTitle(it), - date = it.updatedDate?.localisedFormat("MMM dd").orEmpty() + courseLabel = if (it.isCourseNotification()) getCourseName(it.courseId) else null, + date = it.updatedDate, + isRead = it.isReadState, + route = if (it.assignment?.htmlUrl != null) { + NotificationRoute.DeepLink(it.assignment?.htmlUrl!!) + } else { + NotificationRoute.DeepLink(it.htmlUrl) + } ) } + val globalNotifications = repository.getGlobalAnnouncements(forceRefresh).map { + NotificationItem( + category = NotificationItemCategory( + context.getString(R.string.notificationsAnnouncementCategoryLabel), + StatusChipColor.Sky + ), + title = it.subject, + courseLabel = null, + date = it.startDate, + isRead = true, + route = NotificationRoute.ExplicitRoute( + HorizonInboxRoute.InboxDetails.route( + type = HorizonInboxItemType.AccountNotification, + id = it.id, + courseId = null + ) + ) + ) + } + _uiState.update { it.copy( - allNotificationItems = items, - pagedNotificationItems = items.chunked(10), + notificationItems = (userNotifications + globalNotifications).sortedByDescending { item -> item.date } ) } } @@ -108,54 +131,47 @@ class NotificationViewModel @Inject constructor( } } - private suspend fun getNotificationItemCategoryLabel(streamItem: StreamItem): String { - val courseName = repository.getCourse(streamItem.courseId).name - if (isNotificationItemScored(streamItem)) { - return context.getString(R.string.notificationsAssignmentScoredCategoryLabel) + private fun getNotificationItemCategoryLabel(streamItem: StreamItem): NotificationItemCategory { + if (streamItem.isNotificationItemScored()) { + return NotificationItemCategory( + context.getString(R.string.notificationsScoreChangedCategoryLabel), + StatusChipColor.Violet + ) } - if (isDueDateChanged(streamItem)) { - return context.getString(R.string.notificationsDueDateChangedCategoryLabel) + if (streamItem.isGradingPeriodNotification()) { + return NotificationItemCategory( + context.getString(R.string.notificationsScoreCategoryLabel), + StatusChipColor.Violet + ) } - if (isGradingWeightChanged(streamItem)) { - return context.getString(R.string.notificationsScoringWeightChangedCategoryLabel) + if (streamItem.isDueDateNotification()) { + return NotificationItemCategory( + context.getString(R.string.notificationsDueDateCategoryLabel), + StatusChipColor.Honey + ) } - if (streamItem.contextType == CanvasContext.Type.COURSE) { - return context.getString( - R.string.notificationsAnnouncementFromCetegoryLabel, - courseName + if (streamItem.isCourseNotification()) { + return NotificationItemCategory( + context.getString(R.string.notificationsAnnouncementCategoryLabel), + StatusChipColor.Sky ) } - return streamItem.notificationCategory + return NotificationItemCategory( + streamItem.notificationCategory, + StatusChipColor.Honey + ) } - private suspend fun getNotificationItemTitle(streamItem: StreamItem): String { - val courseName = repository.getCourse(streamItem.courseId).name - if (isNotificationItemScored(streamItem)) { + private fun getNotificationItemTitle(streamItem: StreamItem): String { + if (streamItem.isNotificationItemScored()) { return context.getString(R.string.notificationsScoredItemTitle, streamItem.title) } - if (isDueDateChanged(streamItem)) { - return formatDueDateTitle(streamItem, courseName) - } - if (isAssignmentCreated(streamItem)) { - return streamItem.title?.replace(", $courseName", "").orEmpty() - } - if (isGradingWeightChanged(streamItem)) { - return formatGradingWeightChangeTitle(streamItem) - } return streamItem.title.orEmpty() } - private fun increasePageIndex() { - _uiState.update { - it.copy(currentPageIndex = it.currentPageIndex + 1) - } - } - - private fun decreasePageIndex() { - _uiState.update { - it.copy(currentPageIndex = it.currentPageIndex - 1) - } + private suspend fun getCourseName(courseId: Long): String { + return repository.getCourse(courseId).name } private fun dismissSnackbar() { @@ -163,42 +179,4 @@ class NotificationViewModel @Inject constructor( it.copy(screenState = it.screenState.copy(snackbarMessage = null)) } } - - // TODO: There is no API support for handling categories and titles. - // TODO: For now the Web and iOS logic is copied, but it won't work if language support is introduced. - // TODO: This should be handled in the API. - private fun isNotificationItemScored(streamItem: StreamItem): Boolean { - return streamItem.grade != null || streamItem.score != -1.0 - } - - private fun isDueDateChanged(streamItem: StreamItem): Boolean { - return streamItem.notificationCategory == "Due Date" - && streamItem.title?.contains("Assignment Due Date Changed").orDefault() - } - - private fun isAssignmentCreated(streamItem: StreamItem): Boolean { - return streamItem.notificationCategory == "Due Date" - && streamItem.title?.contains("Assignment Created").orDefault() - } - - private fun isGradingWeightChanged(streamItem: StreamItem): Boolean { - return streamItem.notificationCategory == "Grading Policies" - || streamItem.title?.contains("Grading Weight Changed").orDefault() - } - - private fun formatDueDateTitle(streamItem: StreamItem, courseName: String): String { - val assignmentName = streamItem.title - ?.replace("Assignment Due Date Changed: ", "") - ?.replace(", $courseName", "") - - val dateComponent = streamItem.getMessage(context)?.split("\n\n")?.firstOrNull() - val date = dateComponent.orEmpty() - - return context.getString(R.string.notificationsDueOnTitle, assignmentName, date) - } - - private fun formatGradingWeightChangeTitle(streamItem: StreamItem): String { - val courseNameFromTitle = streamItem.title?.replace("Grade Weight Changed: ", "") - return context.getString(R.string.notificationsScoreWeightChangedTitle, courseNameFromTitle) - } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/StreamItemExtensions.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/StreamItemExtensions.kt new file mode 100644 index 0000000000..242475e0b7 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/StreamItemExtensions.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.notification + +import com.instructure.canvasapi2.models.StreamItem + +internal fun StreamItem.isNotificationItemScored(): Boolean { + return this.grade != null || this.score != -1.0 +} + +internal fun StreamItem.isDueDateNotification(): Boolean { + return this.notificationCategory == "Due Date" +} + +internal fun StreamItem.isCourseNotification(): Boolean { + return this.type == "Announcement" +} + +internal fun StreamItem.isGradingPeriodNotification(): Boolean { + return this.notificationCategory == "Grading Policies" +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/foundation/HorizonColors.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/foundation/HorizonColors.kt index bda4805f4a..af8f653b7c 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/foundation/HorizonColors.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/foundation/HorizonColors.kt @@ -266,6 +266,7 @@ object HorizonColors { } object PrimitivesSky { + val sky12 = Color(0xFFDDECF3) @Composable fun sky30() = colorResource(R.color.primitives_sky30) @Composable @@ -282,6 +283,7 @@ object HorizonColors { fun sky70() = colorResource(R.color.primitives_sky70) @Composable fun sky90() = colorResource(R.color.primitives_sky90) + val sky90 = Color(0xFF0F4E6A) @Composable fun sky110() = colorResource(R.color.primitives_sky110) } @@ -298,6 +300,7 @@ object HorizonColors { } object PrimitivesViolet { + val violet12 = Color(0xFFF1E6F5) @Composable fun violet30() = colorResource(R.color.primitives_violet30) @Composable @@ -314,6 +317,7 @@ object HorizonColors { fun violet70() = colorResource(R.color.primitives_violet70) @Composable fun violet90() = colorResource(R.color.primitives_violet90) + val violet90 = Color(0xFF682F82) @Composable fun violet110() = colorResource(R.color.primitives_violet110) } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/StatusChip.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/StatusChip.kt index 542d9d721e..a7445de04b 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/StatusChip.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/StatusChip.kt @@ -71,6 +71,16 @@ sealed class StatusChipColor(val contentColor: Color, val fillColor: Color = Col contentColor = HorizonColors.Text.title(), fillColor = HorizonColors.Surface.pageSecondary() ) + + data object Sky : StatusChipColor( + contentColor = HorizonColors.PrimitivesSky.sky90, + fillColor = HorizonColors.PrimitivesSky.sky12 + ) + + data object Violet : StatusChipColor( + contentColor = HorizonColors.PrimitivesViolet.violet90, + fillColor = HorizonColors.PrimitivesViolet.violet12 + ) } @Composable @@ -113,6 +123,24 @@ private fun StatusChipPreviewGreen() { ) } +@Composable +@Preview(showBackground = true) +private fun StatusChipPreviewSky() { + StatusChipPreview( + color = StatusChipColor.Sky, + iconRes = R.drawable.check_circle_full + ) +} + +@Composable +@Preview(showBackground = true) +private fun StatusChipPreviewViolet() { + StatusChipPreview( + color = StatusChipColor.Violet, + iconRes = null + ) +} + @Composable @Preview(showBackground = true) private fun StatusChipPreviewGrey() { @@ -150,7 +178,7 @@ private fun StatusChipPreviewWhite() { } @Composable -private fun StatusChipPreview(color: StatusChipColor, iconRes: Int) { +private fun StatusChipPreview(color: StatusChipColor, iconRes: Int?) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { StatusChip( state = StatusChipState( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/scaffolds/HorizonScaffold.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/scaffolds/HorizonScaffold.kt index 356b647a49..814f47b373 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/scaffolds/HorizonScaffold.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/scaffolds/HorizonScaffold.kt @@ -36,10 +36,12 @@ import com.instructure.horizon.horizonui.organisms.topappbar.HorizonTopAppBar fun HorizonScaffold( title: String, onBackPressed: () -> Unit, + snackbarHost: @Composable () -> Unit = {}, content: @Composable (Modifier) -> Unit, ) { Scaffold( topBar = { HorizonTopAppBar(title, onBackPressed) }, + snackbarHost = snackbarHost, contentColor = HorizonColors.Surface.pagePrimary() ) { innerPadding -> Box( diff --git a/libs/horizon/src/main/res/values/strings.xml b/libs/horizon/src/main/res/values/strings.xml index f4000d422d..91a3718391 100644 --- a/libs/horizon/src/main/res/values/strings.xml +++ b/libs/horizon/src/main/res/values/strings.xml @@ -171,9 +171,10 @@ Failed to refresh notifications Failed to load notifications %1$s\'s score is now available - Assignment scored - Due date changed - Scoring weight changed + Score changed + Due date + Score + Announcement Announcement from %1$s %1$s\'s score weight was changed %1$s is due on %2$s @@ -352,5 +353,8 @@ Failed to enroll in course. Failed to load programs and courses. Failed to refresh programs and courses. + Today + Yesterday + Unable to open Notification There are no external tools for this course. \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/DateExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/DateExtensions.kt index 57901842f9..ff651747e3 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/DateExtensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/DateExtensions.kt @@ -71,6 +71,14 @@ fun Date.isPreviousDay(date: Date?): Boolean { return calendar.time.isSameDay(this) } +fun Date.isSameWeek(date: Date?): Boolean { + if (date == null) return false + val calendar = Calendar.getInstance() + calendar.time = date + calendar.add(Calendar.WEEK_OF_YEAR, -1) + return this.after(calendar.time) +} + fun Date.getLastSunday(): Date { val calendar = Calendar.getInstance() calendar.time = this From f8b52ae0bfb5f5a547af14dc339f555b2f274cfd Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:07:52 +0200 Subject: [PATCH 05/94] [MBL-19322][Student][Teacher] Assignments are sorted by Assignment ID instead of the due date refs: MBL-19322 affects: Student, Teacher release note: Assignments on Assignment list are now sorted by due date. --- .../list/AssignmentListViewModel.kt | 17 +++- .../list/AssignmentListViewModelTest.kt | 97 ++++++++++++++++++- 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt index 7b1ad27314..450a7ae584 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt @@ -289,7 +289,15 @@ class AssignmentListViewModel @Inject constructor( } } - filteredAssignments = filteredAssignments.sortedBy { it.id }.distinct() + filteredAssignments = filteredAssignments + .sortedWith( + compareBy( + { it.dueDateIncludingCheckpoints() == null }, + { it.dueDateIncludingCheckpoints() }, + { it.id } + ) + ) + .distinct() val groups = when(selectedFilters.selectedGroupByOption) { AssignmentGroupByOption.DueDate -> { @@ -326,12 +334,13 @@ class AssignmentListViewModel @Inject constructor( } AssignmentGroupByOption.AssignmentGroup -> { filteredAssignments.groupBy { it.assignmentGroupId }.map { (key, value) -> - uiState.value.assignmentGroups.firstOrNull { it.id == key }?.name.orEmpty() to value.map { + val group = uiState.value.assignmentGroups.firstOrNull { it.id == key } + group?.position.orDefault() to (group?.name.orEmpty() to value.map { assignmentListBehavior.getAssignmentGroupItemState( course, it, customStatuses, getCheckpoints(it, course) ) - } - }.toMap() + }) + }.sortedBy { it.first }.associate { it.second } } AssignmentGroupByOption.AssignmentType -> { val discussionsGroup = filteredAssignments.filter { diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModelTest.kt index d703044eb1..c548ce57d2 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModelTest.kt @@ -545,7 +545,7 @@ class AssignmentListViewModelTest { var newFilter = AssignmentListSelectedFilters(selectedGroupByOption = AssignmentGroupByOption.AssignmentGroup) viewModel.handleAction(AssignmentListScreenEvent.UpdateFilterState(newFilter)) - assertEquals(assignmentGroups.map { it.assignments }, viewModel.uiState.value.listState.values.map { it.map { it.assignment } } ) + assertEquals(assignmentGroups.reversed().map { it.assignments }, viewModel.uiState.value.listState.values.map { it.map { it.assignment } } ) newFilter = newFilter.copy(selectedGroupByOption = AssignmentGroupByOption.DueDate) viewModel.handleAction(AssignmentListScreenEvent.UpdateFilterState(newFilter)) @@ -692,6 +692,101 @@ class AssignmentListViewModelTest { Assert.assertTrue(viewModel.uiState.value.listState.values.first().first().checkpointsExpanded) } + @Test + fun `Test assignments are sorted by due date and id`() = runTest { + val now = Date() + val pastDate = Date(now.time - 100000) + val futureDate = Date(now.time + 100000) + + val assignment1 = Assignment( + id = 1, + name = "Assignment 1", + dueAt = null + ) + val assignment2 = Assignment( + id = 2, + name = "Assignment 2", + dueAt = futureDate.toApiString() + ) + val assignment3 = Assignment( + id = 3, + name = "Assignment 3", + dueAt = pastDate.toApiString() + ) + val assignment4 = Assignment( + id = 4, + name = "Assignment 4", + dueAt = futureDate.toApiString() + ) + val assignmentGroups = listOf( + AssignmentGroup( + id = 1, + name = "Group 1", + assignments = listOf(assignment1, assignment2, assignment3, assignment4) + ) + ) + + val groupItem1 = AssignmentGroupItemState(course, assignment1, emptyList()) + val groupItem2 = AssignmentGroupItemState(course, assignment2, emptyList()) + val groupItem3 = AssignmentGroupItemState(course, assignment3, emptyList()) + val groupItem4 = AssignmentGroupItemState(course, assignment4, emptyList()) + every { behavior.getAssignmentGroupItemState(course, assignment1, any(), any()) } returns groupItem1 + every { behavior.getAssignmentGroupItemState(course, assignment2, any(), any()) } returns groupItem2 + every { behavior.getAssignmentGroupItemState(course, assignment3, any(), any()) } returns groupItem3 + every { behavior.getAssignmentGroupItemState(course, assignment4, any(), any()) } returns groupItem4 + + coEvery { repository.getAssignments(any(), any()) } returns assignmentGroups + + val viewModel = getViewModel() + + viewModel.handleAction(AssignmentListScreenEvent.UpdateFilterState(AssignmentListSelectedFilters(selectedGroupByOption = AssignmentGroupByOption.DueDate))) + + val sortedAssignments = viewModel.uiState.value.listState.values.flatten().map { it.assignment } + assertEquals(listOf(assignment3, assignment2, assignment4, assignment1), sortedAssignments) + } + + @Test + fun `Test assignment groups are sorted by position`() = runTest { + val assignment1 = Assignment( + id = 1, + name = "Assignment 1", + assignmentGroupId = 1 + ) + val assignment2 = Assignment( + id = 2, + name = "Assignment 2", + assignmentGroupId = 2 + ) + val assignmentGroups = listOf( + AssignmentGroup( + id = 1, + name = "Group 1", + assignments = listOf(assignment1), + position = 2 + ), + AssignmentGroup( + id = 2, + name = "Group 2", + assignments = listOf(assignment2), + position = 1 + ) + ) + + val groupItem1 = AssignmentGroupItemState(course, assignment1, emptyList()) + val groupItem2 = AssignmentGroupItemState(course, assignment2, emptyList()) + every { behavior.getAssignmentGroupItemState(course, assignment1, any(), any()) } returns groupItem1 + every { behavior.getAssignmentGroupItemState(course, assignment2, any(), any()) } returns groupItem2 + + coEvery { repository.getAssignments(any(), any()) } returns assignmentGroups + + val viewModel = getViewModel() + + viewModel.handleAction(AssignmentListScreenEvent.UpdateFilterState(AssignmentListSelectedFilters(selectedGroupByOption = AssignmentGroupByOption.AssignmentGroup))) + + val sortedAssignments = viewModel.uiState.value.listState.values.flatten().map { it.assignment } + assertEquals(listOf(assignment2, assignment1), sortedAssignments) + } + private fun getViewModel(): AssignmentListViewModel { return AssignmentListViewModel(savedStateHandle, apiPrefs, resources, repository, behavior) } From ae7e96e6ba43019155d226acb53ff3a3e2486331 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:10:44 +0200 Subject: [PATCH 06/94] [MBL-19279][Teacher] Fixed Classic quiz: If it's not published and tap on the quick SG icon, the app crashes refs: MBL-19279 affects: Teacher release note: Fixed a crash occurred when opening SpeedGrader with an unpublished quiz. --- .../speedgrader/SpeedGraderViewModel.kt | 70 ++++++++++-------- .../speedgrader/SpeedGraderViewModelTest.kt | 74 +++++++++---------- 2 files changed, 78 insertions(+), 66 deletions(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModel.kt index 8415099336..29783b9e30 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModel.kt @@ -17,10 +17,12 @@ package com.instructure.pandautils.features.speedgrader import android.content.Context +import android.content.res.Resources import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.instructure.canvasapi2.models.Assignment +import com.instructure.pandautils.R import com.instructure.pandautils.utils.Const import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -34,7 +36,9 @@ class SpeedGraderViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val repository: SpeedGraderRepository, private val assignmentSubmissionRepository: AssignmentSubmissionRepository, - private val postPolicyRouter: SpeedGraderPostPolicyRouter + private val postPolicyRouter: SpeedGraderPostPolicyRouter, + private val speedGraderErrorHolder: SpeedGraderErrorHolder, + private val resources: Resources ) : ViewModel() { private val assignmentId: Long = savedStateHandle[Const.ASSIGNMENT_ID] @@ -63,9 +67,7 @@ class SpeedGraderViewModel @Inject constructor( val uiState = _uiState.asStateFlow() init { - viewModelScope.launch { - fetchData() - } + fetchData() } private fun navigateToPostPolicy(context: Context) { @@ -78,31 +80,41 @@ class SpeedGraderViewModel @Inject constructor( } } - private suspend fun fetchData() { - _uiState.update { - it.copy(loading = true) - } - assignment = assignmentSubmissionRepository.getAssignment( - assignmentId = assignmentId, - courseId = courseId, - forceNetwork = false - ) - val ids = if (submissionIds.isEmpty()) { - assignmentSubmissionRepository.getGradeableStudentSubmissions( - assignmentId, - courseId, - false - ).map { it.id } - } else { - submissionIds.toList() - } - val assignmentDetails = repository.getAssignmentDetails(assignmentId) - _uiState.update { - it.copy( - assignmentName = assignmentDetails.assignment?.title.orEmpty(), - courseName = assignmentDetails.assignment?.course?.name.orEmpty(), - loading = false, - submissionIds = ids + private fun fetchData() = viewModelScope.launch { + try { + _uiState.update { + it.copy(loading = true) + } + assignment = assignmentSubmissionRepository.getAssignment( + assignmentId = assignmentId, + courseId = courseId, + forceNetwork = false + ) + val ids = if (submissionIds.isEmpty()) { + assignmentSubmissionRepository.getGradeableStudentSubmissions( + assignmentId, + courseId, + false + ).map { it.id } + } else { + submissionIds.toList() + } + val assignmentDetails = repository.getAssignmentDetails(assignmentId) + _uiState.update { + it.copy( + assignmentName = assignmentDetails.assignment?.title.orEmpty(), + courseName = assignmentDetails.assignment?.course?.name.orEmpty(), + loading = false, + submissionIds = ids + ) + } + } catch (e: Exception) { + _uiState.update { + it.copy(loading = false) + } + speedGraderErrorHolder.postError( + message = resources.getString(R.string.generalUnexpectedError), + retryAction = ::fetchData ) } } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModelTest.kt index ed95ace951..e1a478d1ed 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModelTest.kt @@ -16,12 +16,15 @@ */ package com.instructure.pandautils.features.speedgrader +import android.content.res.Resources import androidx.lifecycle.SavedStateHandle import com.instructure.canvasapi2.AssignmentDetailsQuery import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.pandares.R import com.instructure.pandautils.utils.Const import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.mockk import io.mockk.verify import junit.framework.TestCase.assertEquals @@ -46,6 +49,19 @@ class SpeedGraderViewModelTest { private lateinit var assignmentSubmissionRepository: AssignmentSubmissionRepository private lateinit var speedGraderPostPolicyRouter: SpeedGraderPostPolicyRouter private lateinit var savedStateHandle: SavedStateHandle + private val resources: Resources = mockk(relaxed = true) + private val errorHandler: SpeedGraderErrorHolder = mockk(relaxed = true) + + private fun createViewModel() { + viewModel = SpeedGraderViewModel( + savedStateHandle, + repository, + assignmentSubmissionRepository, + speedGraderPostPolicyRouter, + errorHandler, + resources + ) + } @Before fun setUp() = runTest { @@ -70,6 +86,8 @@ class SpeedGraderViewModelTest { any() ) } returns Assignment() + + coEvery { resources.getString(R.string.generalUnexpectedError) } returns "Error" } @After @@ -85,12 +103,7 @@ class SpeedGraderViewModelTest { val assignmentDetails = AssignmentDetailsQuery.Data(assignment = assignment) coEvery { repository.getAssignmentDetails(1L) } returns assignmentDetails - viewModel = SpeedGraderViewModel( - savedStateHandle, - repository, - assignmentSubmissionRepository, - speedGraderPostPolicyRouter - ) + createViewModel() testDispatcher.scheduler.advanceUntilIdle() // Assert @@ -105,12 +118,7 @@ class SpeedGraderViewModelTest { savedStateHandle = SavedStateHandle() assertThrows(IllegalStateException::class.java) { - SpeedGraderViewModel( - savedStateHandle, - repository, - assignmentSubmissionRepository, - speedGraderPostPolicyRouter - ) + createViewModel() } } @@ -119,12 +127,7 @@ class SpeedGraderViewModelTest { savedStateHandle = SavedStateHandle(mapOf(Const.ASSIGNMENT_ID to 1L)) assertThrows(IllegalStateException::class.java) { - SpeedGraderViewModel( - savedStateHandle, - repository, - assignmentSubmissionRepository, - speedGraderPostPolicyRouter - ) + createViewModel() } } @@ -143,12 +146,7 @@ class SpeedGraderViewModelTest { ) ) - viewModel = SpeedGraderViewModel( - savedStateHandle, - repository, - assignmentSubmissionRepository, - speedGraderPostPolicyRouter - ) + createViewModel() // Assert val uiState = viewModel.uiState.value @@ -171,12 +169,7 @@ class SpeedGraderViewModelTest { ) ) - viewModel = SpeedGraderViewModel( - savedStateHandle, - repository, - assignmentSubmissionRepository, - speedGraderPostPolicyRouter - ) + createViewModel() // Assert val uiState = viewModel.uiState.value @@ -185,12 +178,7 @@ class SpeedGraderViewModelTest { @Test fun `Navigating to the post policy screen calls the router`() = runTest { - viewModel = SpeedGraderViewModel( - savedStateHandle, - repository, - assignmentSubmissionRepository, - speedGraderPostPolicyRouter - ) + createViewModel() val uiState = viewModel.uiState.first() @@ -198,4 +186,16 @@ class SpeedGraderViewModelTest { verify { speedGraderPostPolicyRouter.navigateToPostPolicies(any(), any(), any()) } } -} \ No newline at end of file + + @Test + fun `Error posted when fetching data fails`() = runTest { + val exception = Exception("Network error") + coEvery { repository.getAssignmentDetails(1L) } throws exception + + createViewModel() + + coVerify { + errorHandler.postError("Error", any()) + } + } +} From f6b2104b7999f36b24bbb227a95c70bfdf69af13 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:11:43 +0200 Subject: [PATCH 07/94] [MBL-19299][Student][Parent] Excused assignments show missing tag refs: MBL-19299 affects: Student, Parent release note: Excused assignments now consistently show an 'Excused' status in both the assignment list and assignment details. --- .../ui/interaction/AlertsInteractionTest.kt | 2 +- .../SubmissionDetailsInteractionTest.kt | 1 + .../common/pages/AssignmentDetailsPage.kt | 4 ++ .../details/AssignmentDetailsViewModel.kt | 40 +++++-------------- .../pandautils/utils/AssignmentExtensions.kt | 5 ++- .../details/AssignmentDetailsViewModelTest.kt | 10 ++--- 6 files changed, 26 insertions(+), 36 deletions(-) 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 index 8963123059..09200348ba 100644 --- 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 @@ -156,7 +156,7 @@ class AlertsInteractionTest : ParentComposeTest() { alertsPage.clickOnAlert(alert.title) assignmentDetailsPage.assertPageObjects() - assignmentDetailsPage.assertStatusSubmitted() + assignmentDetailsPage.assertStatusLate() } @Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt index 926865ae97..bbd1854e8e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt @@ -96,6 +96,7 @@ class SubmissionDetailsInteractionTest : StudentTest() { submissionDetailsPage.assertCommentDisplayed("Hey!", data.users.values.first()) } + @Stub @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) fun testComments_addCommentToMultipleAttemptSubmission() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt index 5aa1f845fe..427092227c 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt @@ -176,6 +176,10 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti assertStatus(R.string.submitted) } + fun assertStatusLate() { + assertStatus(R.string.lateSubmissionLabel) + } + fun assertStatusNotSubmitted() { assertStatus(R.string.notSubmitted) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt index ff70bf6b11..8125fd72d8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt @@ -56,6 +56,7 @@ import com.instructure.pandautils.features.assignmentdetails.AssignmentDetailsAt import com.instructure.pandautils.features.assignmentdetails.AssignmentDetailsAttemptViewData import com.instructure.pandautils.features.assignments.details.gradecellview.GradeCellViewData import com.instructure.pandautils.features.assignments.details.itemviewmodels.ReminderItemViewModel +import com.instructure.pandautils.features.grades.SubmissionStateLabel import com.instructure.pandautils.features.reminder.ReminderItem import com.instructure.pandautils.features.reminder.ReminderManager import com.instructure.pandautils.features.reminder.ReminderViewState @@ -65,6 +66,7 @@ import com.instructure.pandautils.room.appdatabase.entities.ReminderEntity import com.instructure.pandautils.utils.AssignmentUtils2 import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.HtmlContentFormatter +import com.instructure.pandautils.utils.getSubmissionStateLabel import com.instructure.pandautils.utils.isAudioVisualExtension import com.instructure.pandautils.utils.orDefault import com.instructure.pandautils.utils.toFormattedString @@ -263,35 +265,15 @@ class AssignmentDetailsViewModel @Inject constructor( } val assignmentState = AssignmentUtils2.getAssignmentState(assignment, assignment.submission, false) - - // Don't mark LTI assignments as missing when overdue as they usually won't have a real submission for it - val isMissing = assignment.isMissing() || (assignment.turnInType != Assignment.TurnInType.EXTERNAL_TOOL - && assignment.dueAt != null - && assignmentState == AssignmentUtils2.ASSIGNMENT_STATE_MISSING) - - val matchedCustomStatus = assignment.submission?.customGradeStatusId?.let { id -> - customStatuses.find { it._id.toLongOrNull() == id } - } - - val submittedLabelText = when { - matchedCustomStatus != null -> matchedCustomStatus.name - isMissing -> resources.getString(R.string.missingAssignment) - !assignment.isSubmitted -> resources.getString(R.string.notSubmitted) - assignment.isGraded() -> resources.getString(R.string.gradedSubmissionLabel) - else -> resources.getString(R.string.submitted) - } - - val submissionStatusTint = when { - matchedCustomStatus != null -> R.color.textInfo - assignment.isSubmitted -> R.color.textSuccess - isMissing -> R.color.textDanger - else -> R.color.textDark - } - - val submittedStatusIcon = when { - matchedCustomStatus != null -> R.drawable.ic_flag - assignment.isSubmitted -> R.drawable.ic_complete_solid - else -> R.drawable.ic_no + val submissionStateLabel = assignment + .getSubmissionStateLabel(customStatuses) + .takeUnless { it == SubmissionStateLabel.None } + ?: SubmissionStateLabel.Submitted + val submissionStatusTint = submissionStateLabel.colorRes + val submittedStatusIcon = submissionStateLabel.iconRes + val submittedLabelText = when (submissionStateLabel) { + is SubmissionStateLabel.Predefined -> resources.getString(submissionStateLabel.labelRes) + is SubmissionStateLabel.Custom -> submissionStateLabel.label } // Submission Status under title - We only show Graded or nothing at all for PAPER/NONE diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AssignmentExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AssignmentExtensions.kt index d5be91b514..62a68193ee 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AssignmentExtensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AssignmentExtensions.kt @@ -17,7 +17,6 @@ package com.instructure.pandautils.utils -import android.content.Context import android.content.res.Resources import com.instructure.canvasapi2.CustomGradeStatusesQuery import com.instructure.canvasapi2.models.Assignment @@ -207,6 +206,7 @@ fun Assignment.getSubAssignmentSubmissionGrade( private fun mapSubmissionStateLabel( customGradeStatusId: Long?, customStatuses: List, + excused: Boolean, late: Boolean, missing: Boolean, graded: Boolean, @@ -218,6 +218,7 @@ private fun mapSubmissionStateLabel( } return when { + excused -> SubmissionStateLabel.Excused matchedCustomStatus != null -> SubmissionStateLabel.Custom( R.drawable.ic_flag, R.color.textInfo, @@ -238,6 +239,7 @@ fun Assignment.getSubmissionStateLabel( ) = mapSubmissionStateLabel( submission?.customGradeStatusId, customStatuses, + submission?.excused.orDefault(), submission?.late.orDefault(), isMissing(), isGraded().orDefault(), @@ -251,6 +253,7 @@ fun Assignment.getSubAssignmentSubmissionStateLabel( ) = mapSubmissionStateLabel( submission?.customGradeStatusId, customStatuses, + submission?.excused.orDefault(), submission?.late.orDefault(), submission?.missing.orDefault(), !submission?.grade.isNullOrEmpty(), diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt index 657c9569e8..3b30833801 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt @@ -298,9 +298,9 @@ class AssignmentDetailsViewModelTest { fun `Missing submission`() { val expectedLabelText = "Missing" val expectedTint = R.color.textDanger - val expectedIcon = R.drawable.ic_no + val expectedIcon = R.drawable.ic_unpublish - every { resources.getString(R.string.missingAssignment) } returns expectedLabelText + every { resources.getString(R.string.missingSubmissionLabel) } returns expectedLabelText val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course @@ -322,8 +322,8 @@ class AssignmentDetailsViewModelTest { @Test fun `Not submitted submission`() { val expectedLabelText = "Not submitted" - val expectedTint = R.color.textDark - val expectedIcon = R.drawable.ic_no + val expectedTint = R.color.backgroundDark + val expectedIcon = R.drawable.ic_unpublish every { resources.getString(R.string.notSubmitted) } returns expectedLabelText @@ -373,7 +373,7 @@ class AssignmentDetailsViewModelTest { fun `Submitted submission`() { val expectedLabelText = "Submitted" val expectedTint = R.color.textSuccess - val expectedIcon = R.drawable.ic_complete_solid + val expectedIcon = R.drawable.ic_complete every { resources.getString(R.string.submitted) } returns expectedLabelText From eece501f150aaa473679b81b018f81a09c25db65 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:14:30 +0200 Subject: [PATCH 08/94] [MBL-19372][Parent] - Fix Alert Settings dialog message appearance (#3265) * Fix Alert Settings error message to be shown in every case, regardless of type and the size of the given number. refs: MBL-19298 affects: Parent release note: Always show Alert Settings error message regardless of type and number size. * Revert "Fix Alert Settings error message to be shown in every case, regardless of type and the size of the given number." This reverts commit 7526414c788def73e0e4292ad40d6e62a0d8fcc9. * Fix alert settings threshold to not accept numbers with more than 3 characters. refs: MBL-19372 affects: Parent release note: Limit Alert Settings threshold numbers for maximum 3 characters. --- .../features/alerts/settings/AlertSettingsScreen.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt index 68b1daf8dc..66af9ed9e6 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt @@ -430,7 +430,7 @@ private fun ThresholdDialog( ) { val context = LocalContext.current var percentage by remember { mutableStateOf(threshold.orEmpty()) } - val enabled = percentage.toIntOrNull().orDefault() in (min + 1).. Date: Tue, 30 Sep 2025 14:15:07 +0200 Subject: [PATCH 09/94] [MBL-19309][Teacher] Section filtering does not work by clicking on the text refs: MBL-19309 affects: Teacher release note: Fixed a bug occurred while filtering by section on Submission list. --- .../submission/SubmissionListFiltersScreen.kt | 8 ++++---- .../compose/composables/CheckboxText.kt | 16 ++++++---------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFiltersScreen.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFiltersScreen.kt index dc4d17e81f..08353ee96a 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFiltersScreen.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFiltersScreen.kt @@ -101,7 +101,7 @@ private fun SubmissionFilterScreenContent( ) }.orEmpty()) } - val selectedSections by remember { mutableStateOf(initialSelectedSections.toMutableSet()) } + var selectedSections by remember { mutableStateOf(initialSelectedSections.toSet()) } var error by remember { mutableStateOf(false) } Scaffold( backgroundColor = colorResource(id = R.color.backgroundLightest), @@ -269,10 +269,10 @@ private fun SubmissionFilterScreenContent( selected = selectedSections.contains(section.id), color = courseColor, onCheckedChanged = { - if (selectedSections.contains(section.id)) { - selectedSections.remove(section.id) + selectedSections = if (selectedSections.contains(section.id)) { + selectedSections - section.id } else { - selectedSections.add(section.id) + selectedSections + section.id } }, modifier = Modifier.fillMaxWidth() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/CheckboxText.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/CheckboxText.kt index bae70b64a2..06551b4c6e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/CheckboxText.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/CheckboxText.kt @@ -22,10 +22,6 @@ import androidx.compose.material.Checkbox import androidx.compose.material.CheckboxDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -45,15 +41,15 @@ fun CheckboxText( modifier: Modifier = Modifier, testTag: String = "checkboxText" ) { - var checked by remember { mutableStateOf(selected) } - Row ( - modifier = modifier.clickable { checked = !checked }.padding(vertical = 8.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .clickable { onCheckedChanged(!selected) } + .padding(vertical = 8.dp, horizontal = 16.dp) ) { Checkbox( - checked = checked, + checked = selected, onCheckedChange = { - checked = it onCheckedChanged(it) }, colors = CheckboxDefaults.colors( From 7e6aaca70d6ee3b05239701a6b5ab664dfd223b5 Mon Sep 17 00:00:00 2001 From: Nagy Adam Date: Tue, 30 Sep 2025 15:49:24 +0200 Subject: [PATCH 10/94] [MBL-17359][Student] Implement Course browser Interaction test for home page navigation (#3271) * Implement interaction test for Home page navigation in CourseBrowser. refs: MBL-17359 affects: Student release note: * Add CourseFrontPageEndpoint and interaction test for home page navigation. refs: MBL-17359 affects: Student release note: * PR fix refs: MBL-17359 affects: Student release note: * PR fix refs: MBL-17359 affects: Student release note: --- .../ui/interaction/CourseInteractionTest.kt | 40 +++++++++++++++++-- .../canvas/espresso/mockCanvas/MockCanvas.kt | 6 ++- .../mockCanvas/endpoints/CourseEndpoints.kt | 20 ++++++++++ 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt index f012d8706d..929cfd601b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt @@ -101,7 +101,7 @@ class CourseInteractionTest : StudentTest() { val course = data.courses.values.first() val displayName = "FamousQuote.html" - val fileId = data.addFileToCourse( + data.addFileToCourse( courseId = course.id, displayName = displayName, fileContent = """ @@ -129,21 +129,54 @@ class CourseInteractionTest : StudentTest() { .check(webMatches(getText(), containsString("Ask not"))) } + // Home button should open the front page and display the correct header/title + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.COURSE, TestCategory.INTERACTION) + fun testCourse_homeDisplaysFrontPageTitle() { + + getToCourse(courseCount = 1, favoriteCourseCount = 1) { data, course -> + + data.addPageToCourse( + courseId = course.id, + pageId = 1, + url = "front-page", + title = "Front Page", + body = "

Very interesting Page

", + published = true, + frontPage = true + ) + + data.courses[course.id] = data.courses[course.id]!!.copy(homePage = Course.HomePage.HOME_WIKI) + } + + courseBrowserPage.selectHome() + + onWebView(withId(R.id.contentWebView)) + .withElement(findElement(Locator.ID, "header1")) + .check(webMatches(getText(), containsString("Very interesting Page"))) + } + /** Utility method to create mocked data, sign student 0 in, and navigate to course 0. */ private fun getToCourse( - courseCount: Int = 2, // Need to link from one to the other - favoriteCourseCount: Int = 1): MockCanvas { + courseCount: Int = 2, + favoriteCourseCount: Int = 1, + configure: (MockCanvas, Course) -> Unit = { _, _ -> } + ): MockCanvas { val data = MockCanvas.init( studentCount = 1, courseCount = courseCount, favoriteCourseCount = favoriteCourseCount) val course1 = data.courses.values.first() + val homeTab = Tab(position = 1, label = "Home", visibility = "public", tabId = Tab.HOME_ID) val pagesTab = Tab(position = 2, label = "Pages", visibility = "public", tabId = Tab.PAGES_ID) val filesTab = Tab(position = 3, label = "Files", visibility = "public", tabId = Tab.FILES_ID) + data.courseTabs[course1.id]!! += homeTab data.courseTabs[course1.id]!! += pagesTab data.courseTabs[course1.id]!! += filesTab + configure(data, course1) + val student = data.students[0] val token = data.tokenFor(student)!! tokenLogin(data.domain, token, student) @@ -155,4 +188,3 @@ class CourseInteractionTest : StudentTest() { return data } } - diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt index a194421cf8..19a1659711 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt @@ -1426,7 +1426,8 @@ fun MockCanvas.addPageToCourse( title: String = Randomizer.randomPageTitle(), body: String = Randomizer.randomPageBody(), published: Boolean = false, - groupId: Long? = null + groupId: Long? = null, + frontPage: Boolean = false ): Page { val page = Page( @@ -1434,7 +1435,8 @@ fun MockCanvas.addPageToCourse( url = url, title = title, body = body, - published = published + published = published, + frontPage = frontPage ) var list : MutableList? = null diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt index 48c7ca5ae9..31908158c0 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt @@ -116,6 +116,7 @@ object CourseListEndpoint : Endpoint( object CourseEndpoint : Endpoint( Segment("tabs") to CourseTabsEndpoint, Segment("assignments") to AssignmentIndexEndpoint, + Segment("front_page") to CourseFrontPageEndpoint, Segment("assignment_groups") to AssignmentGroupListEndpoint, Segment("external_tools") to ExternalToolsEndpoint, Segment("pages") to CoursePagesEndpoint, @@ -387,6 +388,25 @@ object CoursePagesEndpoint : Endpoint( } }) +/** + * Endpoint that returns the front page for a course + */ +object CourseFrontPageEndpoint : Endpoint( + response = { + GET { + val courseId = pathVars.courseId + val pages = data.coursePages[courseId] + val front = pages?.firstOrNull { it.frontPage } + if (front != null) { + request.successResponse(front) + } else { + request.unauthorizedResponse() + } + } + } +) + + /** * Endpoint that returns a specific page from a course */ From e23e8920126cabadfe40ec6069a009d754bd0ff2 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:31:49 +0200 Subject: [PATCH 11/94] [MBL-19298][Student] - Remove Stub from testCommentsBelongToSubmissionAttempts E2E test (#3254) * Remove Stub from testCommentsBelongToSubmissionAttempts E2E test. refs: MBL-19331 affects: Student release note: * Remove sleep after sending a comment - attempt to fix on CI. refs: MBL-19298 affects: Student release note: * Add utility function to wait for the WorkManager job to be finished. refs: MBL-19298 affects: Student release note: * Attempt to force workmanager to execute jobs immediately in tests. refs: MBL-19298 affects: Student release note: * Force workmanager job to be finished automatically with TestDriver. Synchronize gradle workmanager testing lib versions. Remove unnecessary duplicated initialization in 'setupWorkerFactory' in StudentTest. (It's already happening in CanvasTest) refs: MBL-19298 affects: Student release note: * Attempt to fix workmanager null exception. refs: MBL-19298 affects: Student release note: * PR suggestion fixes. Extract work manager handling to the global TestingUtils file, so it can be used everywhere. Simplify/beautify code. refs: MBL-19298 affects: Student release note: * Copilot PR suggestion. Stabilize some interaction tests (use other retry logic). refs: MBL-19298 affects: Student release note: * Delete unused TestAppManager class from student. (We are using the global TestAppManager). refs: MBL-19298 affects: Student release note: --- .../src/main/java/GlobalDependencies.kt | 2 ++ apps/student/build.gradle | 1 + .../student/espresso/TestAppManager.kt | 29 ------------------- .../student/ui/e2e/AssignmentsE2ETest.kt | 7 ++--- .../SubmissionDetailsInteractionTest.kt | 12 ++++++-- .../student/ui/utils/StudentTest.kt | 8 ----- .../student/ui/utils/StudentTestExtensions.kt | 2 +- .../mobius/common/ui/SubmissionHelper.kt | 1 + automation/espresso/build.gradle | 1 + .../instructure/canvas/espresso/CanvasTest.kt | 3 +- .../canvas/espresso/TestAppManager.kt | 29 +++++++++++++++++++ .../common/pages/compose/SettingsPage.kt | 3 +- .../com/instructure/espresso/TestingUtils.kt | 29 +++++++++++++++++++ 13 files changed, 81 insertions(+), 46 deletions(-) delete mode 100644 apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt diff --git a/apps/buildSrc/src/main/java/GlobalDependencies.kt b/apps/buildSrc/src/main/java/GlobalDependencies.kt index affae9257d..7120f7076b 100644 --- a/apps/buildSrc/src/main/java/GlobalDependencies.kt +++ b/apps/buildSrc/src/main/java/GlobalDependencies.kt @@ -32,6 +32,7 @@ object Versions { const val LIFECYCLE = "2.8.6" const val FRAGMENT = "1.8.4" const val WORK_MANAGER = "2.9.1" + const val WORK_TEST = "2.9.1" const val GLIDE_VERSION = "4.16.0" const val RETROFIT = "2.11.0" const val OKHTTP = "4.12.0" @@ -75,6 +76,7 @@ object Libs { const val ANDROIDX_CORE_TESTING = "androidx.arch.core:core-testing:2.2.0" const val ANDROIDX_WORK_MANAGER = "androidx.work:work-runtime:${Versions.WORK_MANAGER}" const val ANDROIDX_WORK_MANAGER_KTX = "androidx.work:work-runtime-ktx:${Versions.WORK_MANAGER}" + const val ANDROIDX_WORK_TEST = "androidx.work:work-testing:${Versions.WORK_TEST}" const val ANDROIDX_WEBKIT = "androidx.webkit:webkit:1.9.0" const val ANDROIDX_DATABINDING_COMPILER = "androidx.databinding:databinding-compiler:${Versions.ANDROID_GRADLE_TOOLS}" // This is bundled with the gradle plugin so we use the same version const val ANDROIDX_COMPOSE_ACTIVITY = "androidx.activity:activity-compose:1.9.0" diff --git a/apps/student/build.gradle b/apps/student/build.gradle index fe8ad0cd14..9c288dbd49 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -233,6 +233,7 @@ dependencies { /* Android Test Dependencies */ androidTestImplementation project(path: ':espresso') androidTestImplementation project(':dataseedingapi') + androidTestImplementation Libs.ANDROIDX_WORK_TEST /* Unit Test Dependencies */ testImplementation Libs.JUNIT diff --git a/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt b/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt deleted file mode 100644 index fc00cc637e..0000000000 --- a/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2022 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.student.espresso - -import androidx.work.WorkerFactory -import com.instructure.student.util.BaseAppManager - -open class TestAppManager : BaseAppManager() { - - var workerFactory: WorkerFactory? = null - - override fun getWorkManagerFactory(): WorkerFactory { - return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() - } -} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt index 40942aa0f4..09079ceefb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt @@ -42,6 +42,7 @@ import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 +import com.instructure.espresso.handleWorkManagerTask import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.pandautils.utils.toFormattedString import com.instructure.student.R @@ -806,7 +807,6 @@ class AssignmentsE2ETest: StudentComposeTest() { submissionDetailsPage.assertSelectedAttempt("Attempt 1") } - @Stub @E2E @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.E2E) @@ -882,8 +882,7 @@ class AssignmentsE2ETest: StudentComposeTest() { val newComment = "Comment for second attempt" Log.d(STEP_TAG, "Add a new comment ('$newComment') and send it.") submissionDetailsPage.addAndSendComment(newComment) - - sleep(2000) // Give the comment time to propagate + handleWorkManagerTask("SubmissionWorker") Log.d(ASSERTION_TAG, "Assert that '$newComment' is displayed.") submissionDetailsPage.assertCommentDisplayed(newComment, student) @@ -1184,7 +1183,7 @@ class AssignmentsE2ETest: StudentComposeTest() { SubmissionsApi.gradeSubmission(teacher.token, course.id, passFailAssignment.id, student.id, postedGrade = "Incomplete") Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") - val gpaScaleAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.GPA_SCALE, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) + val gpaScaleAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.GPA_SCALE, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) Log.d(PREPARATION_TAG, "Grade submission: '${gpaScaleAssignment.name}' with 3.7.") SubmissionsApi.gradeSubmission(teacher.token, course.id, gpaScaleAssignment.id, student.id, postedGrade = "3.7") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt index bbd1854e8e..06ec68e8ea 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt @@ -18,7 +18,6 @@ package com.instructure.student.ui.interaction import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.test.junit4.createEmptyComposeRule -import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.webdriver.Locator import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils @@ -45,6 +44,7 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.RubricCriterion import com.instructure.canvasapi2.models.RubricCriterionRating import com.instructure.canvasapi2.models.SubmissionComment +import com.instructure.espresso.handleWorkManagerTask import com.instructure.student.ui.pages.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.tokenLogin @@ -88,11 +88,14 @@ class SubmissionDetailsInteractionTest : StudentTest() { assignmentListPage.clickAssignment(assignment) assignmentDetailsPage.clickSubmit() urlSubmissionUploadPage.submitText("https://google.com") - Espresso.onIdle() + handleWorkManagerTask("SubmissionWorker") + assignmentDetailsPage.assertAssignmentSubmitted() assignmentDetailsPage.goToSubmissionDetails() submissionDetailsPage.openComments() submissionDetailsPage.addAndSendComment("Hey!") + handleWorkManagerTask("SubmissionWorker") + submissionDetailsPage.assertCommentDisplayed("Hey!", data.users.values.first()) } @@ -111,11 +114,14 @@ class SubmissionDetailsInteractionTest : StudentTest() { assignmentListPage.clickAssignment(assignment) assignmentDetailsPage.clickSubmit() urlSubmissionUploadPage.submitText("https://google.com") + handleWorkManagerTask("SubmissionWorker") + assignmentDetailsPage.assertAssignmentSubmitted() assignmentDetailsPage.assertNoAttemptSpinner() assignmentDetailsPage.clickSubmit() urlSubmissionUploadPage.submitText("https://google.com") + handleWorkManagerTask("SubmissionWorker") assignmentDetailsPage.goToSubmissionDetails() @@ -123,6 +129,8 @@ class SubmissionDetailsInteractionTest : StudentTest() { submissionDetailsPage.assertSelectedAttempt("Attempt 1") submissionDetailsPage.openComments() submissionDetailsPage.addAndSendComment("Hey!") + handleWorkManagerTask("SubmissionWorker") + submissionDetailsPage.assertCommentDisplayed("Hey!", data.users.values.first()) submissionDetailsPage.selectAttempt("Attempt 2") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index e10448467b..598f9465ef 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -48,7 +48,6 @@ import com.instructure.pandautils.utils.Const import com.instructure.student.BuildConfig import com.instructure.student.R import com.instructure.student.activity.LoginActivity -import com.instructure.student.espresso.TestAppManager import com.instructure.student.ui.pages.AllCoursesPage import com.instructure.student.ui.pages.AnnotationCommentListPage import com.instructure.student.ui.pages.AnnouncementListPage @@ -104,7 +103,6 @@ import com.instructure.student.ui.pages.offline.SyncProgressPage import instructure.rceditor.RCETextEditor import org.hamcrest.Matcher import org.hamcrest.core.AllOf -import org.junit.Before import java.io.File abstract class StudentTest : CanvasTest() { @@ -181,12 +179,6 @@ abstract class StudentTest : CanvasTest() { val manageOfflineContentPage = ManageOfflineContentPage() val syncProgressPage = SyncProgressPage() - @Before - fun setupWorkerFactory() { - val application = activityRule.activity.application as? TestAppManager - application?.workerFactory = workerFactory - } - // A no-op interaction to afford us an easy, harmless way to get a11y checking to trigger. fun meaninglessSwipe() { Espresso.onView(ViewMatchers.withId(R.id.action_bar_root)).swipeRight() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt index e98329a47f..10691bb62d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt @@ -342,4 +342,4 @@ fun uploadTextFile( token, fileUploadType ) -} +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionHelper.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionHelper.kt index 7a45c793a3..8b4bd3cc1f 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionHelper.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionHelper.kt @@ -45,6 +45,7 @@ class SubmissionHelper( val submissionWork = OneTimeWorkRequest.Builder(SubmissionWorker::class.java) .setInputData(data.build()) .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + .addTag("SubmissionWorker") .build() workManager.enqueue(submissionWork) } diff --git a/automation/espresso/build.gradle b/automation/espresso/build.gradle index 77993530c0..8a48e8502a 100644 --- a/automation/espresso/build.gradle +++ b/automation/espresso/build.gradle @@ -91,6 +91,7 @@ dependencies { implementation project(':dataseedingapi') implementation project(':login-api-2') + implementation Libs.ANDROIDX_WORK_TEST androidTestImplementation Libs.COMPOSE_UI_TEST implementation project(':pandautils') diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt index c0125f35ec..6655d1f772 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt @@ -114,7 +114,8 @@ abstract class CanvasTest : InstructureTestingContract { } val application = originalActivity.application as? TestAppManager - application?.workerFactory = workerFactory + application?.workerFactory = this.workerFactory + application?.initWorkManager(application) } @Before diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt index 3674a850b8..138b08ca27 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt @@ -14,21 +14,50 @@ * limitations under the License. */package com.instructure.canvas.espresso +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import androidx.work.Configuration import androidx.work.WorkerFactory +import androidx.work.testing.SynchronousExecutor +import androidx.work.testing.TestDriver +import androidx.work.testing.WorkManagerTestInitHelper import com.instructure.canvasapi2.AppManager import com.instructure.canvasapi2.utils.RemoteConfigUtils open class TestAppManager: AppManager() { + @SuppressLint("RestrictedApi") override fun onCreate() { super.onCreate() RemoteConfigUtils.initialize() + + if (workerFactory == null) { + workerFactory = WorkerFactory.getDefaultWorkerFactory() + } } + var testDriver: TestDriver? = null + var workerFactory: WorkerFactory? = null + @SuppressLint("RestrictedApi") override fun getWorkManagerFactory(): WorkerFactory { return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() } override fun performLogoutOnAuthError() = Unit + + fun initWorkManager(context: Context) { + try { + val config = Configuration.Builder() + .setMinimumLoggingLevel(Log.DEBUG) + .setExecutor(SynchronousExecutor()) + .setWorkerFactory(getWorkManagerFactory()) + .build() + WorkManagerTestInitHelper.initializeTestWorkManager(context, config) + testDriver = WorkManagerTestInitHelper.getTestDriver(context) + } catch (e: IllegalStateException) { + Log.w("TestAppManager", "WorkManager.initialize() failed, likely already initialized: ${e.message}") + } + } } diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt index fa84278cc2..fec009cb42 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt @@ -37,6 +37,7 @@ import com.instructure.espresso.page.onView import com.instructure.espresso.page.onViewWithText import com.instructure.espresso.page.withText import com.instructure.espresso.retry +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.pandautils.utils.AppTheme class SettingsPage(private val composeTestRule: ComposeTestRule) : BasePage() { @@ -71,7 +72,7 @@ class SettingsPage(private val composeTestRule: ComposeTestRule) : BasePage() { fun clickOnSettingsItem(title: String) { val nodeMatcher = hasTestTag("settingsItem").and(hasAnyDescendant(hasText(title))) - retry(catchBlock = { + retryWithIncreasingDelay(times = 10, catchBlock = { val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val y = device.displayHeight / 2 val x = device.displayWidth / 2 diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt index 0e5873edff..6e4364dc92 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt @@ -19,6 +19,7 @@ import android.os.Build import android.view.View import androidx.annotation.RequiresApi import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.NoMatchingViewException @@ -29,6 +30,9 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.webdriver.DriverAtoms import androidx.test.espresso.web.webdriver.Locator +import androidx.test.platform.app.InstrumentationRegistry +import androidx.work.WorkManager +import com.instructure.canvas.espresso.TestAppManager import com.instructure.espresso.page.plus import com.instructure.pandautils.binding.BindableViewHolder import org.apache.commons.lang3.StringUtils @@ -241,3 +245,28 @@ fun getRecyclerViewFromMatcher(matcher: Matcher): RecyclerView { return recyclerView ?: throw IllegalStateException("Failed to retrieve RecyclerView") } +fun handleWorkManagerTask(workerTag: String) { + val app = ApplicationProvider.getApplicationContext() + val testDriver = app.testDriver!! + + val workInfos = WorkManager.getInstance(app) + .getWorkInfosByTag(workerTag) + .get() + val workInfo = workInfos.find { !it.state.isFinished } + + testDriver.setAllConstraintsMet(workInfo?.id ?: return) + waitForWorkManagerJobsToFinish(workerTag = workerTag) +} + +private fun waitForWorkManagerJobsToFinish(timeoutMs: Long = 20000L, workerTag: String) { + val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext + val workManager = WorkManager.getInstance(context) + val start = System.currentTimeMillis() + while (true) { + val unfinished = workManager.getWorkInfosByTag(workerTag).get() + .any { !it.state.isFinished } + if (!unfinished) break + if (System.currentTimeMillis() - start > timeoutMs) break + Thread.sleep(250) + } +} From ee1d680000c3c9462649889a031e9ddd145cf84b Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:16:18 +0200 Subject: [PATCH 12/94] [MBL-19361][Teacher] Marking 0-point assignments as Complete incorrectly reverts to Incomplete refs: MBL-19361 affects: Teacher release note: Fixed a bug that occurred when grading complete/incomplete assignment in Speed Grader. --- .../grade/grading/SpeedGraderGradingScreen.kt | 15 +++++-- .../grading/SpeedGraderGradingUiState.kt | 1 + .../grading/SpeedGraderGradingViewModel.kt | 39 +++++++++++----- .../SpeedGraderGradingViewModelTest.kt | 45 ++++++++++++++++++- 4 files changed, 84 insertions(+), 16 deletions(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt index c16fe88020..eb9b921d3e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt @@ -526,7 +526,9 @@ private fun LetterGradeGradingTypeInput(uiState: SpeedGraderGradingUiState) { @Composable private fun CompleteIncompleteGradingTypeInput(uiState: SpeedGraderGradingUiState) { val haptic = LocalHapticFeedback.current - var grade by remember { mutableStateOf(uiState.enteredGrade ?: "") } + var grade by remember(uiState.enteredGrade) { + mutableStateOf(uiState.enteredGrade.orEmpty()) + } Column { Row( modifier = Modifier @@ -562,7 +564,7 @@ private fun CompleteIncompleteGradingTypeInput(uiState: SpeedGraderGradingUiStat onClick = { haptic.performHapticFeedback(HapticFeedbackType.LongPress) grade = "complete" - uiState.onScoreChange(uiState.pointsPossible?.toFloat() ?: 0f) + uiState.onCompletionChange(true) }, testtag = "speedGraderCompleteRadioButton" ) @@ -575,11 +577,10 @@ private fun CompleteIncompleteGradingTypeInput(uiState: SpeedGraderGradingUiStat onClick = { haptic.performHapticFeedback(HapticFeedbackType.LongPress) grade = "incomplete" - uiState.onScoreChange(0f) + uiState.onCompletionChange(false) }, testtag = "speedGraderIncompleteRadioButton" ) - } } @@ -849,6 +850,7 @@ private fun SpeedGraderGradingContentPreview() { score = 14.0, pointsDeducted = 1.0, onScoreChange = {}, + onCompletionChange = {}, submittedAt = Date(), daysLate = 4f, gradingType = GradingType.points, @@ -876,6 +878,7 @@ private fun SpeedGraderGradingContentPercentagePreview() { grade = "90%", score = 15.0, onScoreChange = {}, + onCompletionChange = {}, submittedAt = Date(), daysLate = 4f, gradingType = GradingType.percent, @@ -904,6 +907,7 @@ private fun SpeedGraderGradingContentCompleteIncompletePreview() { submittedAt = Date(), daysLate = 4f, onScoreChange = {}, + onCompletionChange = {}, gradingType = GradingType.pass_fail, onPercentageChange = {}, onExcuse = {}, @@ -928,6 +932,7 @@ private fun SpeedGraderGradingContentLetterGraderPreview() { grade = null, score = 15.0, onScoreChange = {}, + onCompletionChange = {}, gradingType = GradingType.letter_grade, onPercentageChange = {}, pointsDeducted = 2.0, @@ -958,6 +963,7 @@ private fun SpeedGraderGradingContentErrorPreview() { error = true, loading = false, onScoreChange = {}, + onCompletionChange = {}, onPercentageChange = {}, onExcuse = {}, onStatusChange = {}, @@ -978,6 +984,7 @@ private fun SpeedGraderGradingContentLoadingPreview() { error = false, loading = true, onScoreChange = {}, + onCompletionChange = {}, onPercentageChange = {}, onExcuse = {}, onStatusChange = {}, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingUiState.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingUiState.kt index 3715f30223..2b0bfbf01a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingUiState.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingUiState.kt @@ -39,6 +39,7 @@ data class SpeedGraderGradingUiState( val gradingStatus: String? = null, val gradeHidden: Boolean = false, val onScoreChange: (Float?) -> Unit, + val onCompletionChange: (Boolean) -> Unit, val onExcuse: () -> Unit, val onPercentageChange: (Float?) -> Unit, val onStatusChange: (GradeStatus) -> Unit, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModel.kt index 4f7d69ebd4..b59c625ac8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModel.kt @@ -36,6 +36,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.math.roundToInt @@ -60,6 +61,7 @@ class SpeedGraderGradingViewModel @Inject constructor( MutableStateFlow( SpeedGraderGradingUiState( onScoreChange = this::onScoreChanged, + onCompletionChange = this::onCompletionChanged, onPercentageChange = this::onPercentageChanged, onExcuse = this::onExcuse, onStatusChange = this::onStatusChange, @@ -166,17 +168,34 @@ class SpeedGraderGradingViewModel @Inject constructor( private fun onScoreChanged(score: Float?) { if (_uiState.value.excused.not() && score == _uiState.value.enteredScore) return - debounceJob?.cancel() + val gradeValue = score?.toString() ?: resources.getString(R.string.not_graded) + submitDebouncedGradeUpdate(gradeValue) { + onScoreChanged(score) + } + } + + private fun onCompletionChanged(complete: Boolean) { + val gradeValue = if (complete) "complete" else "incomplete" + if (_uiState.value.excused.not() && gradeValue == _uiState.value.enteredGrade) return + + submitDebouncedGradeUpdate(gradeValue) { + onCompletionChanged(complete) + } + } + + private fun submitDebouncedGradeUpdate(gradeValue: String, retryAction: () -> Unit) { + debounceJob?.cancel() debounceJob = viewModelScope.launch { delay(300) + try { repository.updateSubmissionGrade( - score = score?.toString() ?: resources.getString(R.string.not_graded), - studentId, - assignmentId, - courseId, - false + score = gradeValue, + userId = studentId, + assignmentId = assignmentId, + courseId = courseId, + excused = false ) AssignmentGradedEvent(assignmentId).postSticky() @@ -188,12 +207,12 @@ class SpeedGraderGradingViewModel @Inject constructor( } speedGraderErrorHolder.postError( message = resources.getString(R.string.generalUnexpectedError), - retryAction = { - onScoreChanged(score) - } + retryAction = retryAction ) } finally { - loadData(true) + if (isActive) { + loadData(true) + } } } } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModelTest.kt index 90206d4400..0a95659cc7 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModelTest.kt @@ -146,6 +146,7 @@ class SpeedGraderGradingViewModelTest { ), gradingStatus = "graded", onScoreChange = {}, + onCompletionChange = {}, onExcuse = {}, onStatusChange = {}, onPercentageChange = {}, @@ -208,6 +209,45 @@ class SpeedGraderGradingViewModelTest { } } + @Test + fun `completion changed`() = runTest { + val submission = createMockSubmission(grade = "incomplete") + + coEvery { repository.getSubmissionGrade(any(), any(), any()) } returns submission + createViewModel() + + val uiState = viewModel.uiState.first() + + assertEquals("incomplete", uiState.grade) + + coEvery { repository.getSubmissionGrade(any(), any(), any()) } returns submission.copy( + submission = submission.submission?.copy(grade = "complete") + ) + + coEvery { + repository.updateSubmissionGrade( + any(), + any(), + any(), + any(), + any() + ) + } returns mockk() + + uiState.onCompletionChange(true) + + testDispatcher.scheduler.advanceTimeBy(600) + coVerify { + repository.updateSubmissionGrade( + "complete", + userId = studentId, + assignmentId = assignmentId, + courseId = courseId, + excused = false + ) + } + } + @Test fun `percentage changed`() = runTest { val submission = createMockSubmission() @@ -370,12 +410,13 @@ class SpeedGraderGradingViewModelTest { private fun createMockSubmission( dueDate: Date = Date(), - status: String = "graded" + status: String = "graded", + grade: String? = "A" ): SubmissionGradeQuery.Data { return SubmissionGradeQuery.Data( submission = SubmissionGradeQuery.Submission( gradingStatus = SubmissionGradingStatus.graded, - grade = "A", + grade = grade, gradeHidden = false, _id = "123", submissionStatus = "submitted", From e825acbbc845e28471e8b5e59525d4156f7c31e8 Mon Sep 17 00:00:00 2001 From: andrasmaczak Date: Thu, 2 Oct 2025 12:21:30 +0200 Subject: [PATCH 13/94] [MBL-19378][Student][Teacher] Dash playback fix refs: MBL-19378 affects: Student, Teacher release note: none --- .../student/activity/BaseRouterActivity.kt | 2 +- .../student/activity/VideoViewActivity.kt | 4 +- .../content/MediaSubmissionViewFragment.kt | 2 +- .../teacher/fragments/ViewMediaFragment.kt | 4 +- .../activities/BaseViewMediaActivity.kt | 2 +- .../pandautils/utils/RouteUtils.kt | 38 +++++++---- .../pandautils/unit/RouteUtilsTest.kt | 67 +++++++++++-------- 7 files changed, 71 insertions(+), 48 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt index 7f61d016b4..54d92ece0f 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt @@ -317,7 +317,7 @@ abstract class BaseRouterActivity : CallbackActivity(), FullScreenInteractions { } private suspend fun shouldOpenInternally(url: String): Boolean { - val mediaUrl = RouteUtils.getRedirectUrl(Uri.parse(url)).toString() + val mediaUrl = RouteUtils.getMediaUri(Uri.parse(url)).toString() return (mediaUrl.endsWith(".mpd") || mediaUrl.endsWith(".m3u8") || mediaUrl.endsWith(".mp4")) } diff --git a/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt index 37dd826bce..bf58280176 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt @@ -86,7 +86,7 @@ class VideoViewActivity : BaseCanvasActivity() { private fun fetchMediaUri(uri: Uri) { lifecycleScope.launch { - val mediaUri = RouteUtils.getRedirectUrl(uri) + val mediaUri = RouteUtils.getMediaUri(uri) player = ExoPlayer.Builder(this@VideoViewActivity) .setTrackSelector(trackSelector) .setLoadControl(DefaultLoadControl()) @@ -100,7 +100,7 @@ class VideoViewActivity : BaseCanvasActivity() { private fun buildMediaSource(uri: Uri): MediaSource { val mediaItem = MediaItem.fromUri(uri) - return when (val type = Util.inferContentType(uri.lastPathSegment ?: "")) { + return when (val type = Util.inferContentType(uri)) { C.CONTENT_TYPE_SS -> SsMediaSource.Factory(DefaultSsChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)).createMediaSource(mediaItem) C.CONTENT_TYPE_DASH -> DashMediaSource.Factory(DefaultDashChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)).createMediaSource(mediaItem) C.CONTENT_TYPE_HLS -> HlsMediaSource.Factory(DefaultHlsDataSourceFactory(buildDataSourceFactory(false))).createMediaSource(mediaItem) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt index f49e8fb39c..81201f736a 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt @@ -116,7 +116,7 @@ class MediaSubmissionViewFragment : BaseCanvasFragment() { private fun fetchMediaUri() { lifecycleScope.launch { - mediaUri = RouteUtils.getRedirectUrl(uri) + mediaUri = RouteUtils.getMediaUri(uri) if (isResumed) { attachMediaPlayer() } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt index fd8acd8c58..caa5545e69 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt @@ -82,7 +82,7 @@ class ViewMediaFragment : BaseCanvasFragment(), ShareableFile { private var editableFile: EditableFile? by NullableParcelableArg() private var mediaUri: Uri? = null - private val exoAgent get() = ExoAgent.getAgentForUri(uri) + private val exoAgent get() = ExoAgent.getAgentForUri(mediaUri ?: uri) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_speed_grader_media, container, false) @@ -144,7 +144,7 @@ class ViewMediaFragment : BaseCanvasFragment(), ShareableFile { private fun fetchMediaUri() { lifecycleScope.launch { - mediaUri = RouteUtils.getRedirectUrl(uri) + mediaUri = RouteUtils.getMediaUri(uri) if (isResumed) { attachMediaPlayer() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/activities/BaseViewMediaActivity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/activities/BaseViewMediaActivity.kt index eafa73e1e7..07732b7aaf 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/activities/BaseViewMediaActivity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/activities/BaseViewMediaActivity.kt @@ -93,7 +93,7 @@ abstract class BaseViewMediaActivity : BaseCanvasActivity() { mediaProgressBar.setVisible() } lifecycleScope.launch { - mediaUri = RouteUtils.getRedirectUrl(mUri) + mediaUri = RouteUtils.getMediaUri(mUri) attachMediaPlayer() } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/RouteUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/RouteUtils.kt index fcc801f4a6..b5267f1eeb 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/RouteUtils.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/RouteUtils.kt @@ -25,6 +25,9 @@ import com.instructure.interactions.router.RouterParams import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.Request +import androidx.core.net.toUri +import com.instructure.canvasapi2.builders.RestParams +import okhttp3.Response object RouteUtils { fun retrieveFileUrl( @@ -48,38 +51,47 @@ object RouteUtils { block.invoke(fileUrl, context, needsAuth) } - suspend fun getRedirectUrl(uri: Uri): Uri { - if (!uri.toString().contains("redirect=")) { - return uri - } - return withContext(Dispatchers.IO) { + suspend fun getMediaUri(uri: Uri): Uri { + var response: Response? = null + val responseUri = withContext(Dispatchers.IO) { try { val client = CanvasRestAdapter.okHttpClient .newBuilder() - .followRedirects(false) + .followRedirects(true) .cache(null) .build() val request = Request.Builder() + .head() .url(uri.toString()) + .tag(RestParams(disableFileVerifiers = false)) .build() - val response = client.newCall(request).execute() + response = client.newCall(request).execute() response.use { - return@withContext if (response.isRedirect) { - val header = response.header("Location") - if (header != null) { - Uri.parse(header) + var responseUrl = response.request.url.toString().toUri() + if (responseUrl.toString().isEmpty()) { + responseUrl = uri + } + val contentTypeHeader = response.header("content-type") + if (contentTypeHeader != null) { + if (contentTypeHeader.contains("dash") && !responseUrl.toString() + .endsWith(".mpd") + ) { + ("$responseUrl.mpd").toUri() } else { - uri + responseUrl } } else { - uri + responseUrl } } } catch (e: Exception) { + response?.close() return@withContext uri } } + response?.close() + return responseUri } } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/unit/RouteUtilsTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/unit/RouteUtilsTest.kt index e3eee44df4..37dd4d2582 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/unit/RouteUtilsTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/unit/RouteUtilsTest.kt @@ -26,7 +26,9 @@ import com.instructure.interactions.router.RouterParams import com.instructure.pandautils.utils.RouteUtils import io.mockk.* import kotlinx.coroutines.runBlocking +import okhttp3.HttpUrl import okhttp3.OkHttpClient +import okhttp3.Request import okhttp3.Response import org.junit.Assert import org.junit.Before @@ -37,15 +39,17 @@ class RouteUtilsTest : Assert() { lateinit var route: Route lateinit var user: User - private val notRedirectUri: Uri = mockk(relaxed = true) - private val redirectUri: Uri = mockk(relaxed = true) - private val redirectedUri: Uri = mockk(relaxed = true) - private val notRedirectUrl = "https://domain.com/file" - private val redirectUrl = "https://domain.com/file?redirect=1" - private val redirectedUrl = "https://domain.com/redirected" + private val requestUri: Uri = mockk(relaxed = true) + private val responseUri: Uri = mockk(relaxed = true) + private val dashUri: Uri = mockk(relaxed = true) + private val requestUrl = "https://domain.com/mediaUrl" + private val responseUrl = "https://domain.com/file" + private val dashUrl = "https://domain.com/file.mpd" private val okHttpClient: OkHttpClient = mockk(relaxed = true) private val call = mockk() private val response = mockk() + private val request = mockk() + private val httpUrl = mockk() @Before fun setup() { @@ -57,21 +61,26 @@ class RouteUtilsTest : Assert() { every { ApiPrefs.fullDomain } returns "https://domain.instructure.com" mockkStatic(Uri::class) - every { notRedirectUri.toString() } returns notRedirectUrl - every { redirectUri.toString() } returns redirectUrl - every { redirectedUri.toString() } returns redirectedUrl - every { Uri.parse(notRedirectUrl) } returns notRedirectUri - every { Uri.parse(redirectUrl) } returns redirectUri - every { Uri.parse(redirectedUrl) } returns redirectedUri + every { requestUri.toString() } returns requestUrl + every { responseUri.toString() } returns responseUrl + every { dashUri.toString() } returns dashUrl + + every { Uri.parse(requestUrl) } returns requestUri + every { Uri.parse(responseUrl) } returns responseUri + every { Uri.parse(dashUrl) } returns dashUri every { okHttpClient.newCall(any()) } returns call - every { okHttpClient.newBuilder().followRedirects(false) + every { okHttpClient.newBuilder().followRedirects(true) .cache(null).build() } returns okHttpClient - every { response.isRedirect } returns true - every { response.header("Location") } returns redirectedUrl + every { httpUrl.toString() } returns responseUrl + every { request.url } returns httpUrl + every { response.request } returns request + every { response.header("content-type") } returns "application/dash+xml" every { response.close() } just Runs coEvery { call.execute() } returns response + + mockkObject(com.instructure.canvasapi2.CanvasRestAdapter) every { com.instructure.canvasapi2.CanvasRestAdapter.okHttpClient } returns okHttpClient } @@ -155,30 +164,32 @@ class RouteUtilsTest : Assert() { } @Test - fun `returns original uri if no redirect param`() = runBlocking { - val result = RouteUtils.getRedirectUrl(notRedirectUri) - assertEquals(notRedirectUri, result) + fun `getMediaUri returns proper dash url if content-type is dash`() = runBlocking { + val result = RouteUtils.getMediaUri(requestUri) + assertEquals(dashUri, result) } @Test - fun `returns redirected uri if response is redirect`() = runBlocking { - val result = RouteUtils.getRedirectUrl(redirectUri) - assertEquals(redirectedUri, result) + fun `getMediaUri returns responseUri if if content-type is not dash`() = runBlocking { + every { response.header("content-type") } returns "application/mp4" + + val result = RouteUtils.getMediaUri(responseUri) + assertEquals(responseUri, result) } @Test - fun `returns original uri if response is not redirect`() = runBlocking { - every { response.isRedirect } returns false + fun `getMediaUri returns responseUri if if content-type is null`() = runBlocking { + every { response.header("content-type") } returns null - val result = RouteUtils.getRedirectUrl(redirectUri) - assertEquals(redirectUri, result) + val result = RouteUtils.getMediaUri(responseUri) + assertEquals(responseUri, result) } @Test - fun `returns original uri on exception`() = runBlocking { + fun `getMediaUri returns original uri on exception`() = runBlocking { coEvery { call.execute() } throws Exception("Network error") - val result = RouteUtils.getRedirectUrl(redirectUri) - assertEquals(redirectUri, result) + val result = RouteUtils.getMediaUri(requestUri) + assertEquals(requestUri, result) } } From a94c47d60fff57d06f6d752031509874b3fb8cc8 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Thu, 2 Oct 2025 13:02:01 +0200 Subject: [PATCH 14/94] [MBL-19303][Teacher] Teachers logged in on root account in a Consortia cannot see past attempts on assignment submissions refs: MBL-19303 affects: Teacher release note: Fixed an issue where teachers logged in under a root account in a Consortia could not view past attempts on assignment submissions. --- .../teacher/presenters/DashboardPresenter.kt | 11 +++++ .../fakes/FakeSubmissionContentManager.kt | 3 +- .../fakes/FakeSubmissionGradeManager.kt | 3 +- .../instructure/canvasapi2/QLClientConfig.kt | 45 +++++++++++++++---- .../instructure/canvasapi2/apis/CourseAPI.kt | 2 +- .../graphql/SubmissionContentManager.kt | 2 +- .../graphql/SubmissionContentManagerImpl.kt | 5 ++- .../graphql/SubmissionGradeManager.kt | 7 ++- .../graphql/SubmissionGradeManagerImpl.kt | 11 +++-- .../content/SpeedGraderContentRepository.kt | 10 +++-- .../content/SpeedGraderContentViewModel.kt | 17 +++++-- .../grading/SpeedGraderGradingRepository.kt | 6 +-- .../grading/SpeedGraderGradingViewModel.kt | 13 ++++-- .../SpeedGraderContentViewModelTest.kt | 4 +- .../SpeedGraderGradingViewModelTest.kt | 4 +- 15 files changed, 106 insertions(+), 37 deletions(-) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DashboardPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/DashboardPresenter.kt index 537fd0ac8a..07ae69e090 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DashboardPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/DashboardPresenter.kt @@ -18,6 +18,8 @@ package com.instructure.teacher.presenters import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DashboardCard +import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.weave.apiAsync import com.instructure.pandautils.blueprint.SyncPresenter import com.instructure.pandautils.utils.ColorApiHelper @@ -54,6 +56,7 @@ class DashboardPresenter : SyncPresenter(Course::class.java .onSuccess { courses -> // Make a call to get which courses are visible on the dashboard as well as their position loadCards(forceNetwork, courses) + storeDomainOverrides(courses) } } @@ -69,6 +72,14 @@ class DashboardPresenter : SyncPresenter(Course::class.java } } + private fun storeDomainOverrides(courses: List) { + courses.forEach { course -> + course.tabs?.find { it.tabId == Tab.ASSIGNMENTS_ID }?.domain?.let { + ApiPrefs.overrideDomains[course.id] = it + } + } + } + private fun createCourseFromDashboardCard(dashboardCard: DashboardCard, courseMap: Map): Course { val course = courseMap[dashboardCard.id] return if (course != null) { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionContentManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionContentManager.kt index e8910ef9e5..648610459b 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionContentManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionContentManager.kt @@ -26,7 +26,8 @@ import com.instructure.canvasapi2.type.SubmissionType class FakeSubmissionContentManager : SubmissionContentManager { override suspend fun getSubmissionContent( userId: Long, - assignmentId: Long + assignmentId: Long, + domain: String? ): SubmissionContentQuery.Data { val assignment = MockCanvas.data.assignments[assignmentId] val submission = MockCanvas.data.submissions[assignmentId]?.get(0) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionGradeManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionGradeManager.kt index 6e74881772..f4b6b8823f 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionGradeManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionGradeManager.kt @@ -30,7 +30,8 @@ class FakeSubmissionGradeManager : SubmissionGradeManager { override suspend fun getSubmissionGrade( assignmentId: Long, studentId: Long, - forceNetwork: Boolean + forceNetwork: Boolean, + domain: String? ): SubmissionGradeQuery.Data { val assignment = MockCanvas.data.assignments[assignmentId] val course = MockCanvas.data.courses[assignment?.courseId] diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/QLClientConfig.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/QLClientConfig.kt index 4b6f755405..04643be4b3 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/QLClientConfig.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/QLClientConfig.kt @@ -38,9 +38,11 @@ import com.instructure.canvasapi2.utils.toApiString import com.instructure.canvasapi2.utils.toDate import okhttp3.OkHttpClient import java.io.File -import java.util.* +import java.util.Date import java.util.concurrent.TimeUnit +private const val DOMAIN_HEADER_KEY = "Override-Domain" + open class QLClientConfig { /** The GraphQL endpoint. Defaults to "/api/graphql/" */ @@ -54,6 +56,18 @@ open class QLClientConfig { .addInterceptor { chain -> chain.proceed(chain.request().newBuilder().addHeader("GraphQL-Metrics", "true").build()) } + .addInterceptor { chain -> + var request = chain.request() + val domain = request.header(DOMAIN_HEADER_KEY) + if (domain != null) { + val newUrl = (domain + GRAPHQL_ENDPOINT) + request = request.newBuilder() + .url(newUrl) + .removeHeader(DOMAIN_HEADER_KEY) + .build() + } + chain.proceed(request) + } .build() var fetchPolicy: HttpFetchPolicy = HttpFetchPolicy.CacheFirst @@ -126,16 +140,31 @@ open class QLClientConfig { suspend fun ApolloClient.enqueueQuery( query: Query, - forceNetwork: Boolean = false + forceNetwork: Boolean = false, + domain: String? = null ): ApolloResponse { - if (forceNetwork) { - return this.query(query).httpFetchPolicy(HttpFetchPolicy.NetworkOnly).executeV3() + val call = if (forceNetwork) { + this.query(query).httpFetchPolicy(HttpFetchPolicy.NetworkOnly) + } else { + this.query(query) + } + + if (domain != null) { + call.addHttpHeader(DOMAIN_HEADER_KEY, domain) } - return this.query(query).executeV3() + + return call.executeV3() } -suspend fun ApolloClient.enqueueMutation( - mutation: Mutation +suspend fun ApolloClient.enqueueMutation( + mutation: Mutation, + domain: String? = null ): ApolloResponse { - return this.mutation(mutation).executeV3() + val call = this.mutation(mutation) + + if (domain != null) { + call.addHttpHeader(DOMAIN_HEADER_KEY, domain) + } + + return call.executeV3() } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt index 9e2559322b..3516f2163d 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt @@ -75,7 +75,7 @@ object CourseAPI { @get:GET("courses?include[]=term&include[]=syllabus_body&include[]=license&include[]=is_public&include[]=permissions&enrollment_state=active") val firstPageCoursesWithSyllabusWithActiveEnrollment: Call> - @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&include[]=settings&state[]=completed&state[]=available&state[]=unpublished") + @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&include[]=settings&state[]=completed&state[]=available&state[]=unpublished&include[]=tabs") val firstPageCoursesTeacher: Call> @GET("courses/{courseId}?include[]=term&include[]=permissions&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=course_image") diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManager.kt index 083c64f9f1..b02119fe92 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManager.kt @@ -19,5 +19,5 @@ import com.instructure.canvasapi2.SubmissionContentQuery interface SubmissionContentManager { - suspend fun getSubmissionContent(userId: Long, assignmentId: Long): SubmissionContentQuery.Data + suspend fun getSubmissionContent(userId: Long, assignmentId: Long, domain: String? = null): SubmissionContentQuery.Data } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManagerImpl.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManagerImpl.kt index e147af7997..4071a64367 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManagerImpl.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManagerImpl.kt @@ -25,7 +25,8 @@ class SubmissionContentManagerImpl(private val apolloClient: ApolloClient) : Sub override suspend fun getSubmissionContent( userId: Long, - assignmentId: Long + assignmentId: Long, + domain: String? ): SubmissionContentQuery.Data { var hasNextPage = true var nextCursor: String? = null @@ -41,7 +42,7 @@ class SubmissionContentManagerImpl(private val apolloClient: ApolloClient) : Sub nextCursor = if (nextCursor != null) Optional.present(nextCursor) else Optional.absent() ) - data = apolloClient.enqueueQuery(query, forceNetwork = true).dataAssertNoErrors + data = apolloClient.enqueueQuery(query, forceNetwork = true, domain = domain).dataAssertNoErrors val connection = data.submission?.submissionHistoriesConnection allEdges.addAll(connection?.edges.orEmpty()) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManager.kt index d1a678d165..14e0f0da88 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManager.kt @@ -20,7 +20,12 @@ import com.instructure.canvasapi2.UpdateSubmissionStatusMutation interface SubmissionGradeManager { - suspend fun getSubmissionGrade(assignmentId: Long, studentId: Long, forceNetwork: Boolean = false): SubmissionGradeQuery.Data + suspend fun getSubmissionGrade( + assignmentId: Long, + studentId: Long, + forceNetwork: Boolean, + domain: String? = null + ): SubmissionGradeQuery.Data suspend fun updateSubmissionGrade(score: Double, submissionId: Long): UpdateSubmissionGradeMutation.Data diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManagerImpl.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManagerImpl.kt index 5d53d9ca2b..7454156a16 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManagerImpl.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManagerImpl.kt @@ -28,7 +28,8 @@ class SubmissionGradeManagerImpl(private val apolloClient: ApolloClient) : Submi override suspend fun getSubmissionGrade( assignmentId: Long, studentId: Long, - forceNetwork: Boolean + forceNetwork: Boolean, + domain: String? ): SubmissionGradeQuery.Data { var hasNextPage = true var nextCursor: String? = null @@ -36,11 +37,9 @@ class SubmissionGradeManagerImpl(private val apolloClient: ApolloClient) : Submi var submission: SubmissionGradeQuery.Data? = null while (hasNextPage) { - val nextCursorParam = - if (nextCursor != null) Optional.present(nextCursor) else Optional.absent() - val query = - SubmissionGradeQuery(studentId.toString(), assignmentId.toString(), nextCursorParam) - val data = apolloClient.enqueueQuery(query, forceNetwork = true).dataAssertNoErrors + val nextCursorParam = if (nextCursor != null) Optional.present(nextCursor) else Optional.absent() + val query = SubmissionGradeQuery(studentId.toString(), assignmentId.toString(), nextCursorParam) + val data = apolloClient.enqueueQuery(query, forceNetwork = true, domain = domain).dataAssertNoErrors if (submission == null) { submission = data } else { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentRepository.kt index acc7d24319..8b55febc0d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentRepository.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentRepository.kt @@ -31,8 +31,8 @@ class SpeedGraderContentRepository( private val canvaDocsApi: CanvaDocsAPI.CanvaDocsInterFace ) { - suspend fun getSubmission(assignmentId: Long, studentId: Long): SubmissionContentQuery.Data { - return submissionContentManager.getSubmissionContent(studentId, assignmentId) + suspend fun getSubmission(assignmentId: Long, studentId: Long, domain: String? = null): SubmissionContentQuery.Data { + return submissionContentManager.getSubmissionContent(studentId, assignmentId, domain) } suspend fun getSingleSubmission(courseId: Long, assignmentId: Long, studentId: Long): Submission? { @@ -41,8 +41,10 @@ class SpeedGraderContentRepository( suspend fun createCanvaDocSession( submissionId: String, - attempt: String + attempt: String, + domain: String? = null ): CanvaDocSessionResponseBody { - return canvaDocsApi.createCanvaDocSession(CanvaDocSessionRequestBody(submissionId, attempt), RestParams(isForceReadFromNetwork = true)).dataOrThrow + val params = RestParams(isForceReadFromNetwork = true, domain = domain) + return canvaDocsApi.createCanvaDocSession(CanvaDocSessionRequestBody(submissionId, attempt), params).dataOrThrow } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentViewModel.kt index ab4e262ba6..9dc53f54c8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentViewModel.kt @@ -31,6 +31,7 @@ import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.QuizSubmission import com.instructure.canvasapi2.type.SubmissionState import com.instructure.canvasapi2.type.SubmissionStatusTagType +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.validOrNull import com.instructure.pandautils.R import com.instructure.pandautils.features.grades.SubmissionStateLabel @@ -51,6 +52,7 @@ import javax.inject.Inject @HiltViewModel class SpeedGraderContentViewModel @Inject constructor( savedStateHandle: SavedStateHandle, + apiPrefs: ApiPrefs, private val repository: SpeedGraderContentRepository, private val resources: Resources, private val speedGraderSelectedAttemptHolder: SpeedGraderSelectedAttemptHolder, @@ -62,6 +64,9 @@ class SpeedGraderContentViewModel @Inject constructor( private val assignmentId: Long = savedStateHandle.get(ASSIGNMENT_ID_KEY) ?: -1L private val studentId: Long = savedStateHandle.get(STUDENT_ID_KEY) ?: -1L + private val courseId: Long = savedStateHandle.get("courseId") ?: -1L + + private val overrideDomain = apiPrefs.overrideDomains[courseId] init { viewModelScope.launch { @@ -81,7 +86,7 @@ class SpeedGraderContentViewModel @Inject constructor( } private suspend fun fetchData() { - val submission = repository.getSubmission(assignmentId, studentId) + val submission = repository.getSubmission(assignmentId, studentId, overrideDomain) val submissionFields = submission.submission?.submissionFields val groupSubmission = submissionFields?.groupId != null && !submissionFields.assignment?.gradeGroupStudentsIndividually.orDefault() @@ -149,7 +154,7 @@ class SpeedGraderContentViewModel @Inject constructor( } private suspend fun updateSubmissionState() { - val submission = repository.getSubmission(assignmentId, studentId) + val submission = repository.getSubmission(assignmentId, studentId, overrideDomain) val submissionFields = submission.submission?.submissionFields _uiState.update { state -> @@ -258,10 +263,14 @@ class SpeedGraderContentViewModel @Inject constructor( SubmissionType.STUDENT_ANNOTATION -> { try { - val canvaDocSession = repository.createCanvaDocSession(submission?._id.orEmpty(), submissionFields.attempt.toString()) + val canvaDocSession = repository.createCanvaDocSession( + submission?._id.orEmpty(), + submissionFields.attempt.toString(), + overrideDomain + ) PdfContent( canvaDocSession.canvadocsSessionUrl.orEmpty(), - submissionFields.assignment?.courseId?.toLong(), + courseId, (submissionFields.groupId ?: submission?.userId)?.toLong() ) } catch (e: Exception) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingRepository.kt index 28ce766a37..95b64378b1 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingRepository.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingRepository.kt @@ -30,9 +30,10 @@ class SpeedGraderGradingRepository( suspend fun getSubmissionGrade( assignmentId: Long, studentId: Long, - forceNetwork: Boolean + forceNetwork: Boolean, + domain: String? = null ): SubmissionGradeQuery.Data { - return submissionGradeManager.getSubmissionGrade(assignmentId, studentId, forceNetwork) + return submissionGradeManager.getSubmissionGrade(assignmentId, studentId, forceNetwork, domain) } suspend fun updateSubmissionGrade( @@ -66,7 +67,6 @@ class SpeedGraderGradingRepository( ).dataOrThrow } - suspend fun updateSubmissionStatus( submissionId: Long, customStatusId: String? = null, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModel.kt index b59c625ac8..999ac3a45b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.instructure.canvasapi2.models.GradingSchemeRow import com.instructure.canvasapi2.type.CourseGradeStatus +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.R import com.instructure.pandautils.features.speedgrader.SpeedGraderErrorHolder import com.instructure.pandautils.features.speedgrader.grade.GradingEvent @@ -44,6 +45,7 @@ import kotlin.math.roundToInt @HiltViewModel class SpeedGraderGradingViewModel @Inject constructor( savedStateHandle: SavedStateHandle, + apiPrefs: ApiPrefs, private val repository: SpeedGraderGradingRepository, private val resources: Resources, private val gradingEventHandler: SpeedGraderGradingEventHandler, @@ -76,6 +78,8 @@ class SpeedGraderGradingViewModel @Inject constructor( private var daysLateDebounceJob: Job? = null + private val overrideDomain = apiPrefs.overrideDomains[courseId] + init { loadData() viewModelScope.launch { @@ -98,9 +102,12 @@ class SpeedGraderGradingViewModel @Inject constructor( private fun loadData(forceNetwork: Boolean = false) { viewModelScope.launch { try { - val submission = - repository.getSubmissionGrade(assignmentId, studentId, forceNetwork).submission - ?: throw IllegalStateException("Submission not found") + val submission = repository.getSubmissionGrade( + assignmentId, + studentId, + forceNetwork, + overrideDomain + ).submission ?: throw IllegalStateException("Submission not found") submissionId = submission._id _uiState.update { it.copy( diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentViewModelTest.kt index d5e90637be..bbb50a0752 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/content/SpeedGraderContentViewModelTest.kt @@ -24,6 +24,7 @@ import com.instructure.canvasapi2.fragment.SubmissionFields import com.instructure.canvasapi2.models.canvadocs.CanvaDocSessionResponseBody import com.instructure.canvasapi2.type.SubmissionState import com.instructure.canvasapi2.type.SubmissionType +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.pandares.R import com.instructure.pandautils.features.grades.SubmissionStateLabel @@ -68,6 +69,7 @@ class SpeedGraderContentViewModelTest { private val assignment = mockk(relaxed = true) private val resources = mockk(relaxed = true) private val gradingEventHandler = SpeedGraderGradingEventHandler() + private val apiPrefs = mockk(relaxed = true) @Before fun setup() { @@ -107,7 +109,7 @@ class SpeedGraderContentViewModelTest { } private fun createViewModel() { - viewModel = SpeedGraderContentViewModel(savedStateHandle, repository, resources, selectedAttemptHolder, gradingEventHandler) + viewModel = SpeedGraderContentViewModel(savedStateHandle, apiPrefs, repository, resources, selectedAttemptHolder, gradingEventHandler) } @Test diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModelTest.kt index 0a95659cc7..e25e6e606a 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingViewModelTest.kt @@ -9,6 +9,7 @@ import com.instructure.canvasapi2.type.CourseGradeStatus import com.instructure.canvasapi2.type.GradingType import com.instructure.canvasapi2.type.LatePolicyStatusType import com.instructure.canvasapi2.type.SubmissionGradingStatus +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.R import com.instructure.pandautils.features.speedgrader.SpeedGraderErrorHolder import com.instructure.pandautils.features.speedgrader.grade.GradingEvent @@ -40,6 +41,7 @@ class SpeedGraderGradingViewModelTest { private val gradingEventHandler = SpeedGraderGradingEventHandler() private val resources: Resources = mockk(relaxed = true) private val errorHandler: SpeedGraderErrorHolder = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) private val testDispatcher = UnconfinedTestDispatcher() @@ -65,7 +67,7 @@ class SpeedGraderGradingViewModelTest { } private fun createViewModel() { - viewModel = SpeedGraderGradingViewModel(savedStateHandle, repository, resources, gradingEventHandler, errorHandler) + viewModel = SpeedGraderGradingViewModel(savedStateHandle, apiPrefs, repository, resources, gradingEventHandler, errorHandler) } @After From 7f836fe0e8e206bb5fd8261037b05659244374a1 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:30:50 +0200 Subject: [PATCH 15/94] [MBL-19363][All] Inbox messages can be sent from course participation courses in closed terms refs: MBL-19363 affects: Student, Parent, Teacher release note: Users are no longer able to send Inbox messages from courses that have been concluded or are part of a past term. --- libs/pandares/src/main/res/values/strings.xml | 1 + .../inbox/compose/InboxComposeViewModel.kt | 26 ++++ .../compose/InboxComposeViewModelTest.kt | 132 ++++++++++++++++++ 3 files changed, 159 insertions(+) diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index af60986709..68e112c8fb 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -2153,4 +2153,5 @@ Additional replies (%d) Discussion Checkpoints Multiple Due Dates + Course concluded. Unable to send messages! diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt index 9ca5cc44da..56fbe6364f 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Recipient import com.instructure.canvasapi2.type.EnrollmentType import com.instructure.canvasapi2.utils.DataResult @@ -47,6 +48,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.util.Date import java.util.EnumMap import java.util.UUID import javax.inject.Inject @@ -532,12 +534,31 @@ class InboxComposeViewModel @Inject constructor( return inboxComposeRepository.getRecipients(searchQuery, contextId, forceRefresh) } + private fun canSendMessageInContext(canvasContext: CanvasContext): Boolean { + if (canvasContext !is Course) return true + + val fullCourse = uiState.value.selectContextUiState.canvasContexts + .filterIsInstance() + .find { it.id == canvasContext.id } + ?: canvasContext + + val now = Date() + return fullCourse.workflowState != Course.WorkflowState.COMPLETED && + fullCourse.endDate?.before(now) != true && + fullCourse.term?.endDate?.before(now) != true + } + private fun createConversation() { uiState.value.selectContextUiState.selectedCanvasContext?.let { canvasContext -> viewModelScope.launch { _uiState.update { uiState.value.copy(screenState = ScreenState.Loading) } try { + if (!canSendMessageInContext(canvasContext)) { + sendScreenResult(context.getString(R.string.courseConcludedError)) + return@launch + } + inboxComposeRepository.createConversation( recipients = uiState.value.recipientPickerUiState.selectedRecipients, subject = uiState.value.subject.text, @@ -569,6 +590,11 @@ class InboxComposeViewModel @Inject constructor( _uiState.update { uiState.value.copy(screenState = ScreenState.Loading) } try { + if (!canSendMessageInContext(canvasContext)) { + sendScreenResult(context.getString(R.string.courseConcludedError)) + return@launch + } + inboxComposeRepository.addMessage( conversationId = uiState.value.previousMessages?.conversation?.id ?: 0, recipients = uiState.value.recipientPickerUiState.selectedRecipients, diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModelTest.kt index a66547a723..bd771f7b09 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModelTest.kt @@ -900,6 +900,138 @@ class InboxComposeViewModelTest { // endregion + // region Concluded Course Validation + + @Test + fun `Send message fails when course is concluded`() = runTest { + val concludedCourse = Course(id = 1, name = "Concluded Course", workflowState = Course.WorkflowState.COMPLETED) + + coEvery { inboxComposeRepository.getCourses(any()) } returns DataResult.Success(listOf(concludedCourse)) + coEvery { inboxComposeRepository.getGroups(any()) } returns DataResult.Success(emptyList()) + coEvery { context.getString(R.string.courseConcludedError) } returns "This course has concluded. You can no longer send messages." + + val viewmodel = getViewModel() + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewmodel.events.toList(events) + } + + viewmodel.handleAction(ContextPickerActionHandler.ContextClicked(concludedCourse)) + viewmodel.handleAction(InboxComposeActionHandler.AddRecipient(Recipient(stringId = "1"))) + viewmodel.handleAction(InboxComposeActionHandler.SubjectChanged(TextFieldValue("Subject"))) + viewmodel.handleAction(InboxComposeActionHandler.BodyChanged(TextFieldValue("Body"))) + viewmodel.handleAction(InboxComposeActionHandler.SendClicked) + + coVerify(exactly = 0) { inboxComposeRepository.createConversation(any(), any(), any(), any(), any(), any()) } + assertEquals(InboxComposeViewModelAction.ShowScreenResult("This course has concluded. You can no longer send messages."), events.last()) + } + + @Test + fun `Send message succeeds when course is active`() = runTest { + val activeCourse = Course(id = 1, name = "Active Course", workflowState = Course.WorkflowState.AVAILABLE) + + coEvery { inboxComposeRepository.getCourses(any()) } returns DataResult.Success(listOf(activeCourse)) + coEvery { inboxComposeRepository.getGroups(any()) } returns DataResult.Success(emptyList()) + coEvery { inboxComposeRepository.createConversation(any(), any(), any(), any(), any(), any()) } returns DataResult.Success(mockk()) + + val viewmodel = getViewModel() + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewmodel.events.toList(events) + } + + viewmodel.handleAction(ContextPickerActionHandler.ContextClicked(activeCourse)) + viewmodel.handleAction(InboxComposeActionHandler.AddRecipient(Recipient(stringId = "1"))) + viewmodel.handleAction(InboxComposeActionHandler.SubjectChanged(TextFieldValue("Subject"))) + viewmodel.handleAction(InboxComposeActionHandler.BodyChanged(TextFieldValue("Body"))) + viewmodel.handleAction(InboxComposeActionHandler.SendClicked) + + coVerify(exactly = 1) { inboxComposeRepository.createConversation(any(), any(), any(), any(), any(), any()) } + assertEquals(3, events.size) + assertEquals(InboxComposeViewModelAction.UpdateParentFragment, events[0]) + assertEquals(InboxComposeViewModelAction.ShowScreenResult(context.getString(R.string.messageSentSuccessfully)), events[1]) + assertEquals(InboxComposeViewModelAction.NavigateBack, events[2]) + } + + @Test + fun `Reply message fails when course is concluded`() = runTest { + val concludedCourse = Course(id = 1, name = "Concluded Course", workflowState = Course.WorkflowState.COMPLETED) + val conversation = Conversation(id = 2) + val messages = listOf(Message(id = 2), Message(id = 3)) + + coEvery { inboxComposeRepository.getCourses(any()) } returns DataResult.Success(listOf(concludedCourse)) + coEvery { inboxComposeRepository.getGroups(any()) } returns DataResult.Success(emptyList()) + coEvery { context.getString(R.string.courseConcludedError) } returns "This course has concluded. You can no longer send messages." + + val savedStateHandle = mockk(relaxed = true) + coEvery { savedStateHandle.get(InboxComposeOptions.COMPOSE_PARAMETERS) } returns InboxComposeOptions( + mode = InboxComposeOptionsMode.REPLY, + previousMessages = InboxComposeOptionsPreviousMessages(conversation, messages), + defaultValues = InboxComposeOptionsDefaultValues( + contextCode = concludedCourse.contextId, + contextName = concludedCourse.name + ) + ) + + val viewmodelWithReply = InboxComposeViewModel(savedStateHandle, context, mockk(relaxed = true), inboxComposeRepository, attachmentDao, featureFlagProvider, inboxComposeBehavior) + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewmodelWithReply.events.toList(events) + } + + viewmodelWithReply.handleAction(ContextPickerActionHandler.ContextClicked(concludedCourse)) + viewmodelWithReply.handleAction(InboxComposeActionHandler.AddRecipient(Recipient(stringId = "1"))) + viewmodelWithReply.handleAction(InboxComposeActionHandler.BodyChanged(TextFieldValue("Reply Body"))) + viewmodelWithReply.handleAction(InboxComposeActionHandler.SendClicked) + + coVerify(exactly = 0) { inboxComposeRepository.addMessage(any(), any(), any(), any(), any(), any()) } + assertEquals(InboxComposeViewModelAction.ShowScreenResult("This course has concluded. You can no longer send messages."), events.last()) + } + + @Test + fun `Reply message succeeds when course is active`() = runTest { + val activeCourse = Course(id = 1, name = "Active Course", workflowState = Course.WorkflowState.AVAILABLE) + val conversation = Conversation(id = 2) + val messages = listOf(Message(id = 2), Message(id = 3)) + + coEvery { inboxComposeRepository.getCourses(any()) } returns DataResult.Success(listOf(activeCourse)) + coEvery { inboxComposeRepository.getGroups(any()) } returns DataResult.Success(emptyList()) + coEvery { inboxComposeRepository.addMessage(any(), any(), any(), any(), any(), any()) } returns DataResult.Success(mockk()) + + val savedStateHandle = mockk(relaxed = true) + coEvery { savedStateHandle.get(InboxComposeOptions.COMPOSE_PARAMETERS) } returns InboxComposeOptions( + mode = InboxComposeOptionsMode.REPLY, + previousMessages = InboxComposeOptionsPreviousMessages(conversation, messages), + defaultValues = InboxComposeOptionsDefaultValues( + contextCode = activeCourse.contextId, + contextName = activeCourse.name + ) + ) + + val viewmodelWithReply = InboxComposeViewModel(savedStateHandle, context, mockk(relaxed = true), inboxComposeRepository, attachmentDao, featureFlagProvider, inboxComposeBehavior) + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewmodelWithReply.events.toList(events) + } + + viewmodelWithReply.handleAction(ContextPickerActionHandler.ContextClicked(activeCourse)) + viewmodelWithReply.handleAction(InboxComposeActionHandler.AddRecipient(Recipient(stringId = "1"))) + viewmodelWithReply.handleAction(InboxComposeActionHandler.BodyChanged(TextFieldValue("Reply Body"))) + viewmodelWithReply.handleAction(InboxComposeActionHandler.SendClicked) + + coVerify(exactly = 1) { inboxComposeRepository.addMessage(any(), any(), any(), any(), any(), any()) } + assertEquals(3, events.size) + assertEquals(InboxComposeViewModelAction.UpdateParentFragment, events[0]) + assertEquals(InboxComposeViewModelAction.ShowScreenResult(context.getString(R.string.messageSentSuccessfully)), events[1]) + assertEquals(InboxComposeViewModelAction.NavigateBack, events[2]) + } + + // endregion + private fun getViewModel(fileDownloader: FileDownloader = mockk(relaxed = true)): InboxComposeViewModel { return InboxComposeViewModel(SavedStateHandle(), context, fileDownloader, inboxComposeRepository, attachmentDao, featureFlagProvider, inboxComposeBehavior) } From 49032fb395c7d0dec6757bc7d87b935745f148e2 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:54:07 +0200 Subject: [PATCH 16/94] [MBL-18862][All] - Unifying automation packages and naming conventions (#3247) * Unifying automation packages. Unifying naming conventions. Remove some unnecessary files (some of them were merged into other files, some were unused). refs: MBL-18862 affects: Teacher, Student, Parent release note: * Fix annotations package path in flank files. refs: MBL-18862 affects: Teacher, Student, Parent release note: * Stub (back) report a problem E2E test. (It will be fixed in another ticket) refs: MBL-18862 affects: Teacher, Student, Parent release note: * Rename 'renderTest' and 'renderPages' to 'rendertest' and 'renderpages'. Also, rename 'mockCanvas' to 'mockcanvas'. Resolve conflicts. refs: MBL-18862 affects: Student, Teacher, Parent release note: * Attempt to fix testCommentsBelongToSubmissionAttempts (make testDriver null safe and wait for the workInfo). refs: MBL-18862 affects: Student, Teacher, Parent release note: * Put back TestAppManager to student (attempt to fix breaking test on CI). refs: MBL-18862 affects: Student, Teacher, Parent release note: * print workinfos. refs: MBL-18862 affects: Student, Teacher, Parent release note: * Add logcat log about available workinfos. refs: MBL-18749 affects: Student, Teacher release note: * Refactor handleWorkManagerTask to make sure first the testDriver won't be null before asking for workInfos. refs: MBL-18862 affects: Student, Teacher release note: * Log available workinfos with assertion fails. reset timeout when checking for workinfos. refs: MBL-18862 affects: Student, Teacher release note: * Set local testDriver after re-initialize app.testDriver. refs: MBL-18862 affects: Student, Teacher release note: * Put fix order and put workmanager related test as first in the AssignmentsE2ETest class. refs: MBL-18862 affects: Student, Teacher, Parent release note: * Make sure workmanager related test will be the first to execute. refs: MBL-18862 affects: Student, Teacher, Parent release note: * Fix breaking testComments_previewAttachment test. (Implementing HEAD mock). refs: MBL-18862 affects: Student, Teacher, Parent release note: --- apps/parent/flank.yml | 2 +- apps/parent/flank_coverage.yml | 2 +- apps/parent/flank_e2e.yml | 4 +- apps/parent/flank_landscape.yml | 2 +- apps/parent/flank_multi_api_level.yml | 2 +- apps/parent/flank_tablet.yml | 2 +- .../ui/e2e/{ => classic}/HelpMenuE2ETest.kt | 8 +- .../ui/e2e/{ => compose}/AlertsE2ETest.kt | 20 +- .../{ => compose}/AssignmentDetailsE2ETest.kt | 12 +- .../AssignmentReminderE2ETest.kt | 96 ++--- .../ui/e2e/{ => compose}/CalendarE2ETest.kt | 8 +- .../CourseDetailsFrontPageE2ETest.kt | 8 +- .../CourseDetailsSummaryE2ETest.kt | 8 +- .../CourseDetailsSyllabusE2ETest.kt | 8 +- .../ui/e2e/{ => compose}/CourseListE2ETest.kt | 8 +- .../e2e/{ => compose}/CreateAccountE2ETest.kt | 6 +- .../ui/e2e/{ => compose}/DashboardE2ETest.kt | 8 +- .../ui/e2e/{ => compose}/GradesListE2ETest.kt | 8 +- .../ui/e2e/{ => compose}/InboxE2ETest.kt | 10 +- .../ui/e2e/{ => compose}/LoginE2ETest.kt | 8 +- .../{ => compose}/ManageStudentsE2ETest.kt | 8 +- .../ui/e2e/{ => compose}/SettingsE2ETest.kt | 12 +- .../interaction/AddStudentInteractionTest.kt | 10 +- .../AlertSettingsInteractionTest.kt | 8 +- .../ui/interaction/AlertsInteractionTest.kt | 16 +- .../AssignmentDetailsInteractionTest.kt | 70 ++-- .../CourseDetailsInteractionTest.kt | 6 +- .../ui/interaction/CoursesInteractionTest.kt | 10 +- .../CreateAccountInteractionTest.kt | 6 +- .../interaction/DashboardInteractionTest.kt | 6 +- .../ManageStudentsInteractionTest.kt | 6 +- .../interaction/NotAParentInteractionsTest.kt | 14 +- .../ParentCalendarInteractionTest.kt | 10 +- .../ParentCreateUpdateEventInteractionTest.kt | 8 +- .../ParentCreateUpdateToDoInteractionTest.kt | 8 +- .../ParentEventDetailsInteractionTest.kt | 8 +- .../ParentGradesInteractionTest.kt | 12 +- .../ParentInboxComposeInteractionTest.kt | 32 +- .../ParentInboxDetailsInteractionTest.kt | 14 +- .../ParentInboxListInteractionTest.kt | 12 +- .../ParentInboxSignatureInteractionTest.kt | 28 +- .../ParentSettingsInteractionTest.kt | 10 +- .../ParentToDoDetailsInteractionTest.kt | 8 +- .../ui/interaction/SummaryInteractionTest.kt | 12 +- .../ui/pages/{ => classic}/DashboardPage.kt | 2 +- .../ui/pages/{ => classic}/FrontPagePage.kt | 2 +- .../ui/pages/{ => classic}/HelpPage.kt | 2 +- .../LeftSideNavigationDrawerPage.kt | 2 +- .../{ => compose}/AddStudentBottomPage.kt | 2 +- .../ui/pages/{ => compose}/AlertsPage.kt | 2 +- .../{ => compose}/AnnouncementDetailsPage.kt | 2 +- .../pages/{ => compose}/CourseDetailsPage.kt | 2 +- .../ui/pages/{ => compose}/CoursesPage.kt | 4 +- .../pages/{ => compose}/CreateAccountPage.kt | 2 +- .../pages/{ => compose}/ManageStudentsPage.kt | 2 +- .../ui/pages/{ => compose}/PairingCodePage.kt | 2 +- .../ParentInboxCoursePickerPage.kt | 2 +- .../ui/pages/{ => compose}/QrPairingPage.kt | 2 +- .../{ => compose}/StudentAlertSettingsPage.kt | 2 +- .../ui/pages/{ => compose}/SummaryPage.kt | 2 +- .../ui/pages/{ => compose}/SyllabusPage.kt | 2 +- .../details/AnnouncementDetailsRenderTest.kt} | 4 +- .../alerts/list/AlertsListItemRenderTest.kt} | 6 +- .../alerts/list/AlertsRenderTest.kt} | 4 +- .../settings/AlertSettingsRenderTest.kt} | 6 +- .../details/CourseDetailsRenderTest.kt} | 4 +- .../details/frontpage/FrontPageRenderTest.kt} | 4 +- .../details/summary/SummaryRenderTest.kt} | 4 +- .../courses/list/CoursesRenderTest.kt} | 4 +- .../createaccount/CreateAccountRenderTest.kt} | 4 +- .../ManageStudentsRenderTest.kt} | 4 +- .../notaparent/NotAParentRenderTest.kt} | 4 +- .../parentapp/utils/ParentComposeTest.kt | 28 +- .../instructure/parentapp/utils/ParentTest.kt | 10 +- .../{ => extensions}/ParentTestExtensions.kt | 3 +- apps/student/flank.yml | 2 +- apps/student/flank_coverage.yml | 2 +- apps/student/flank_e2e.yml | 4 +- apps/student/flank_e2e_coverage.yml | 4 +- apps/student/flank_e2e_flaky.yml | 2 +- apps/student/flank_e2e_knownbug.yml | 2 +- apps/student/flank_e2e_lowres.yml | 4 +- apps/student/flank_e2e_min.yml | 4 +- apps/student/flank_e2e_offline.yml | 4 +- apps/student/flank_landscape.yml | 2 +- apps/student/flank_multi_api_level.yml | 2 +- apps/student/flank_tablet.yml | 2 +- .../student/espresso/TestAppManager.kt | 29 ++ .../student/ui/LoginFindSchoolPageTest.kt | 32 -- .../student/ui/LoginLandingPageTest.kt | 34 -- .../student/ui/LoginSignInPageTest.kt | 35 -- .../e2e/{ => classic}/AnnouncementsE2ETest.kt | 8 +- .../{ => classic}/CollaborationsE2ETest.kt | 10 +- .../e2e/{ => classic}/ConferencesE2ETest.kt | 8 +- .../ui/e2e/{ => classic}/DashboardE2ETest.kt | 8 +- .../e2e/{ => classic}/DiscussionsE2ETest.kt | 12 +- .../ui/e2e/{ => classic}/FilesE2ETest.kt | 18 +- .../ui/e2e/{ => classic}/GradesE2ETest.kt | 8 +- .../ui/e2e/{ => classic}/HelpMenuE2ETest.kt | 8 +- .../ui/e2e/{ => classic}/LoginE2ETest.kt | 50 ++- .../ui/e2e/{ => classic}/ModulesE2ETest.kt | 10 +- .../e2e/{ => classic}/NotificationsE2ETest.kt | 8 +- .../ui/e2e/{ => classic}/PagesE2ETest.kt | 10 +- .../{ => classic}/PushNotificationsE2ETest.kt | 4 +- .../ui/e2e/{ => classic}/QuizzesE2ETest.kt | 14 +- .../{ => classic}/ShareExtensionE2ETest.kt | 12 +- .../ui/e2e/{ => classic}/SyllabusE2ETest.kt | 8 +- .../ui/e2e/{ => classic}/TodoE2ETest.kt | 12 +- .../k5/GradesElementaryE2ETest.kt | 10 +- .../{ => classic}/k5/ImportantDatesE2ETest.kt | 13 +- .../e2e/{ => classic}/k5/ScheduleE2ETest.kt | 13 +- .../offline/ManageOfflineContentE2ETest.kt | 11 +- .../offline/OfflineAllCoursesE2ETest.kt | 10 +- .../offline/OfflineAnnouncementsE2ETest.kt | 10 +- .../offline/OfflineAssignmentsE2ETest.kt | 18 +- .../offline/OfflineConferencesE2ETest.kt | 12 +- .../offline/OfflineCourseBrowserE2ETest.kt | 12 +- .../offline/OfflineDashboardE2ETest.kt | 12 +- .../offline/OfflineDiscussionsE2ETest.kt | 18 +- .../offline/OfflineFilesE2ETest.kt | 12 +- .../offline/OfflineGradesE2ETest.kt | 10 +- .../offline/OfflineLeftSideMenuE2ETest.kt | 16 +- .../offline/OfflineLoginE2ETest.kt | 12 +- .../offline/OfflineModulesE2ETest.kt | 14 +- .../offline/OfflinePagesE2ETest.kt | 16 +- .../offline/OfflinePeopleE2ETest.kt | 10 +- .../offline/OfflineSettingsE2ETest.kt | 16 +- .../offline/OfflineSyllabusE2ETest.kt | 10 +- .../offline/OfflineSyncProgressE2ETest.kt | 12 +- .../offline/OfflineSyncSettingsE2ETest.kt | 12 +- .../usergroups/UserGroupFilesE2ETest.kt | 8 +- .../e2e/{ => compose}/AssignmentsE2ETest.kt | 345 +++++++++--------- .../ui/e2e/{ => compose}/BookmarksE2ETest.kt | 16 +- .../student/ui/e2e/compose/CalendarE2ETest.kt | 6 +- .../e2e/{ => compose}/CourseBrowserE2ETest.kt | 8 +- .../ui/e2e/{ => compose}/InboxE2ETest.kt | 10 +- .../ui/e2e/{ => compose}/PeopleE2ETest.kt | 12 +- .../ui/e2e/{ => compose}/SettingsE2ETest.kt | 22 +- .../e2e/{ => compose}/k5/HomeroomE2ETest.kt | 12 +- .../e2e/{ => compose}/k5/ResourcesE2ETest.kt | 10 +- .../AnnouncementInteractionTest.kt | 16 +- .../AssignmentDetailsInteractionTest.kt | 70 ++-- .../AssignmentListInteractionTest.kt | 12 +- .../ui/interaction/BookmarkInteractionTest.kt | 12 +- .../CourseGradesInteractionTest.kt | 10 +- .../ui/interaction/CourseInteractionTest.kt | 10 +- .../interaction/DashboardInteractionTest.kt | 8 +- .../interaction/DiscussionsInteractionTest.kt | 20 +- .../ElementaryDashboardInteractionTest.kt | 8 +- .../ElementaryGradesInteractionTest.kt | 10 +- .../interaction/GroupLinksInteractionTest.kt | 18 +- .../ui/interaction/HomeroomInteractionTest.kt | 16 +- .../ImportantDatesInteractionTest.kt | 18 +- .../interaction/InAppUpdateInteractionTest.kt | 8 +- .../ui/interaction/ModuleInteractionTest.kt | 28 +- .../NavigationDrawerInteractionTest.kt | 8 +- .../NotificationInteractionTest.kt | 14 +- .../OfflineContentInteractionTest.kt | 9 +- .../ui/interaction/PdfInteractionTest.kt | 20 +- .../ui/interaction/PeopleInteractionTest.kt | 8 +- .../PickerSubmissionUploadInteractionTest.kt | 12 +- .../ProfileSettingsInteractionTest.kt | 8 +- .../ui/interaction/QuizListInteractionTest.kt | 8 +- .../interaction/ResourcesInteractionTest.kt | 14 +- .../ui/interaction/ScheduleInteractionTest.kt | 18 +- .../ui/interaction/SettingsInteractionTest.kt | 8 +- .../ShareExtensionInteractionTest.kt | 14 +- .../StudentCalendarInteractionTest.kt | 12 +- ...StudentCreateUpdateEventInteractionTest.kt | 8 +- .../StudentCreateUpdateToDoInteractionTest.kt | 8 +- .../StudentEventDetailsInteractionTest.kt | 8 +- .../StudentInboxComposeInteractionTest.kt | 30 +- .../StudentInboxListInteractionsTest.kt | 12 +- .../StudentInboxSignatureInteractionTest.kt | 26 +- .../StudentSmartSearchInteractionTest.kt | 14 +- .../StudentToDoDetailsInteractionTest.kt | 8 +- .../SubmissionDetailsInteractionTest.kt | 21 +- .../ui/interaction/SyllabusInteractionTest.kt | 14 +- .../SyncSettingsInteractionTest.kt | 7 +- .../ui/interaction/TodoInteractionTest.kt | 16 +- .../interaction/UserFilesInteractionTest.kt | 10 +- .../student/ui/pages/SettingsPage.kt | 0 .../ui/pages/{ => classic}/AllCoursesPage.kt | 2 +- .../AnnotationCommentListPage.kt | 2 +- .../{ => classic}/AnnouncementListPage.kt | 2 +- .../ui/pages/{ => classic}/BookmarkPage.kt | 2 +- .../pages/{ => classic}/CanvasWebViewPage.kt | 2 +- .../pages/{ => classic}/CollaborationsPage.kt | 2 +- .../{ => classic}/ConferenceDetailsPage.kt | 2 +- .../pages/{ => classic}/ConferenceListPage.kt | 2 +- .../ui/pages/{ => classic}/ConferencesPage.kt | 4 +- .../pages/{ => classic}/CourseBrowserPage.kt | 2 +- .../pages/{ => classic}/CourseGradesPage.kt | 2 +- .../ui/pages/{ => classic}/DashboardPage.kt | 8 +- .../{ => classic}/DiscussionDetailsPage.kt | 2 +- .../pages/{ => classic}/DiscussionListPage.kt | 2 +- .../ui/pages/{ => classic}/FileChooserPage.kt | 2 +- .../ui/pages/{ => classic}/FileListPage.kt | 2 +- .../ui/pages/{ => classic}/GoToQuizPage.kt | 2 +- .../ui/pages/{ => classic}/GradesPage.kt | 2 +- .../pages/{ => classic}/GroupBrowserPage.kt | 2 +- .../ui/pages/{ => classic}/HelpPage.kt | 2 +- .../LeftSideNavigationDrawerPage.kt | 2 +- .../{ => classic}/ModuleProgressionPage.kt | 2 +- .../ui/pages/{ => classic}/ModulesPage.kt | 4 +- .../pages/{ => classic}/NotificationPage.kt | 2 +- .../ui/pages/{ => classic}/PageDetailsPage.kt | 2 +- .../ui/pages/{ => classic}/PageListPage.kt | 2 +- .../pages/{ => classic}/PairObserverPage.kt | 2 +- .../ui/pages/{ => classic}/PandaAvatarPage.kt | 2 +- .../ui/pages/{ => classic}/PeopleListPage.kt | 2 +- .../pages/{ => classic}/PersonDetailsPage.kt | 2 +- .../PickerSubmissionUploadPage.kt | 2 +- .../{ => classic}/ProfileSettingsPage.kt | 2 +- .../{ => classic}/PushNotificationsPage.kt | 2 +- .../ui/pages/{ => classic}/QRLoginPage.kt | 2 +- .../ui/pages/{ => classic}/QuizListPage.kt | 2 +- .../ui/pages/{ => classic}/QuizTakingPage.kt | 2 +- .../{ => classic}/RemoteConfigSettingsPage.kt | 2 +- .../{ => classic}/ShareExtensionStatusPage.kt | 2 +- .../{ => classic}/ShareExtensionTargetPage.kt | 2 +- .../StudentAssignmentDetailsPage.kt | 2 +- .../SubmissionDetailsEmptyContentPage.kt | 2 +- .../{ => classic}/SubmissionDetailsPage.kt | 4 +- .../ui/pages/{ => classic}/SyllabusPage.kt | 2 +- .../{ => classic}/TextSubmissionUploadPage.kt | 2 +- .../ui/pages/{ => classic}/TodoPage.kt | 2 +- .../{ => classic}/UrlSubmissionUploadPage.kt | 2 +- .../{ => classic/k5}/ElementaryCoursePage.kt | 2 +- .../k5}/ElementaryDashboardPage.kt | 2 +- .../ui/pages/{ => classic/k5}/HomeroomPage.kt | 2 +- .../{ => classic/k5}/ImportantDatesPage.kt | 2 +- .../pages/{ => classic/k5}/ResourcesPage.kt | 2 +- .../ui/pages/{ => classic/k5}/SchedulePage.kt | 2 +- .../offline/ManageOfflineContentPage.kt | 2 +- .../offline/NativeDiscussionDetailsPage.kt | 4 +- .../offline/OfflineSyncSettingsPage.kt | 2 +- .../{ => classic}/offline/SyncProgressPage.kt | 4 +- .../ConferenceDetailsRenderTest.kt | 4 +- .../ConferenceListRenderTest.kt | 4 +- .../DiscussionSubmissionViewRenderTest.kt | 6 +- .../MediaSubmissionViewRenderTest.kt | 8 +- .../PairObserverRenderTest.kt | 4 +- .../PickerSubmissionUploadRenderTest.kt | 6 +- .../QuizSubmissionViewRenderTest.kt | 6 +- .../SubmissionCommentsRenderTest.kt | 10 +- ...SubmissionDetailsEmptyContentRenderTest.kt | 4 +- .../SubmissionDetailsRenderTest.kt | 6 +- .../SubmissionFilesRenderTest.kt | 6 +- .../SubmissionRubricRenderTest.kt | 8 +- .../SyllabusRenderTest.kt | 6 +- .../TextSubmissionUploadRenderTest.kt | 6 +- .../TextSubmissionViewRenderTest.kt | 8 +- .../UploadStatusSubmissionRenderTest.kt | 6 +- .../UrlSubmissionUploadRenderTest.kt | 4 +- .../UrlSubmissionViewRenderTest.kt | 4 +- .../ConferenceDetailsRenderPage.kt | 6 +- .../renderpages}/ConferenceListRenderPage.kt | 6 +- .../DiscussionSubmissionViewRenderPage.kt | 2 +- .../MediaSubmissionViewRenderPage.kt | 2 +- .../renderpages}/PairObserverRenderPage.kt | 4 +- .../PickerSubmissionUploadRenderPage.kt | 2 +- .../QuizSubmissionViewRenderPage.kt | 2 +- .../SubmissionCommentsRenderPage.kt | 2 +- ...SubmissionDetailsEmptyContentRenderPage.kt | 4 +- .../SubmissionDetailsRenderPage.kt | 4 +- .../renderpages}/SubmissionFilesRenderPage.kt | 2 +- .../SubmissionRubricRenderPage.kt | 2 +- .../renderpages}/SyllabusRenderPage.kt | 4 +- .../TextSubmissionUploadRenderPage.kt | 2 +- .../TextSubmissionViewRenderPage.kt | 2 +- .../UploadStatusSubmissionViewRenderPage.kt | 2 +- .../UrlSubmissionUploadRenderPage.kt | 2 +- .../UrlSubmissionViewRenderPage.kt | 2 +- .../renderpages}/views/GradeCellRenderTest.kt | 4 +- .../instructure/student/ui/utils/Matchers.kt | 176 --------- .../student/ui/utils/StudentComposeTest.kt | 4 +- .../utils}/StudentRenderTest.kt | 15 +- .../student/ui/utils/StudentTest.kt | 107 +++--- .../instructure/student/ui/utils/ViewUtils.kt | 47 --- .../{ => extensions}/StudentTestExtensions.kt | 3 +- .../offline}/OfflineTestUtils.kt | 2 +- apps/teacher/build.gradle | 2 +- apps/teacher/flank.yml | 2 +- apps/teacher/flank_coverage.yml | 2 +- apps/teacher/flank_e2e.yml | 4 +- apps/teacher/flank_e2e_coverage.yml | 4 +- apps/teacher/flank_e2e_flaky.yml | 2 +- apps/teacher/flank_e2e_knownbug.yml | 2 +- apps/teacher/flank_e2e_lowres.yml | 4 +- apps/teacher/flank_e2e_min.yml | 4 +- apps/teacher/flank_landscape.yml | 2 +- apps/teacher/flank_multi_api_level.yml | 2 +- apps/teacher/flank_tablet.yml | 2 +- .../espresso/TeacherHiltTestApplication.kt | 2 +- .../espresso/TeacherHiltTestRunner.kt | 2 +- .../{ui => }/espresso/TestAppManager.kt | 2 +- .../teacher/ui/LoginFindSchoolPageTest.kt | 15 - .../teacher/ui/LoginLandingPageTest.kt | 17 - .../teacher/ui/LoginSignInPageTest.kt | 36 -- .../e2e/{ => classic}/AnnouncementsE2ETest.kt | 8 +- .../{ => classic}/CommentLibraryE2ETest.kt | 8 +- .../{ => classic}/CourseSettingsE2ETest.kt | 8 +- .../ui/e2e/{ => classic}/DashboardE2ETest.kt | 8 +- .../e2e/{ => classic}/DiscussionsE2ETest.kt | 8 +- .../ui/e2e/{ => classic}/FilesE2ETest.kt | 16 +- .../ui/e2e/{ => classic}/HelpMenuE2ETest.kt | 9 +- .../ui/e2e/{ => classic}/LoginE2ETest.kt | 12 +- .../ui/e2e/{ => classic}/ModulesE2ETest.kt | 12 +- .../ui/e2e/{ => classic}/PagesE2ETest.kt | 10 +- .../ui/e2e/{ => classic}/PeopleE2ETest.kt | 22 +- .../{ => classic}/PushNotificationsE2ETest.kt | 4 +- .../ui/e2e/{ => classic}/QuizE2ETest.kt | 14 +- .../ui/e2e/{ => classic}/SettingsE2ETest.kt | 20 +- .../ui/e2e/{ => classic}/SyllabusE2ETest.kt | 12 +- .../ui/e2e/{ => classic}/TodoE2ETest.kt | 22 +- .../ui/e2e/{ => compose}/AssignmentE2ETest.kt | 16 +- .../teacher/ui/e2e/compose/CalendarE2ETest.kt | 8 +- .../ui/e2e/{ => compose}/InboxE2ETest.kt | 10 +- .../e2e/{ => compose}/SpeedGraderE2ETest.kt | 22 +- .../AnnouncementsListInteractionTest.kt} | 14 +- .../AssigneeListInteractionTest.kt} | 14 +- .../AssignmentDetailsInteractionTest.kt} | 20 +- .../AssignmentDueDatesInteractionTest.kt} | 14 +- ...ssignmentSubmissionListInteractionTest.kt} | 20 +- .../CommentLibraryInteractionTest.kt} | 38 +- .../CourseBrowserInteractionTest.kt} | 10 +- .../CourseSettingsInteractionTest.kt} | 10 +- .../DashboardInteractionTest.kt} | 10 +- .../DiscussionsListInteractionTest.kt} | 14 +- .../EditAssignmentDetailsInteractionTest.kt} | 14 +- .../EditDashboardInteractionTest.kt} | 10 +- .../EditQuizDetailsInteractionTest.kt} | 14 +- .../EditSyllabusInteractionTest.kt} | 18 +- .../InAppUpdateInteractionTest.kt} | 9 +- .../ModuleListInteractionTest.kt} | 24 +- .../NavDrawerInteractionTest.kt} | 12 +- .../NotATeacherInteractionTest.kt} | 8 +- .../PageListInteractionTest.kt} | 14 +- .../PersonContextInteractionTest.kt} | 40 +- .../QuizDetailsInteractionTest.kt} | 16 +- .../QuizListInteractionTest.kt} | 14 +- .../QuizSubmissionListInteractionTest.kt} | 22 +- .../SpeedGraderCommentsInteractionTest.kt} | 20 +- .../SpeedGraderFilesInteractionTest.kt} | 42 +-- .../SpeedGraderGradeInteractionTest.kt} | 44 +-- .../SpeedGraderInteractionTest.kt} | 40 +- ...eedGraderQuizSubmissionInteractionTest.kt} | 20 +- .../SyllabusInteractionTest.kt} | 20 +- .../TeacherCalendarInteractionTest.kt} | 16 +- ...cherCalendarToDoDetailsInteractionTest.kt} | 12 +- ...eacherCreateUpdateEventInteractionTest.kt} | 12 +- .../TeacherCreateUpdateToDoInteractionTest.kt | 10 +- .../TeacherEventDetailsInteractionTest.kt} | 12 +- .../TeacherInboxComposeInteractionTest.kt | 34 +- .../TeacherInboxListInteractionTest.kt} | 18 +- .../TeacherInboxSignatureInteractionTest.kt | 34 +- .../UpdateFilePermissionsInteractionTest.kt} | 20 +- .../pages/{ => classic}/AllCoursesListPage.kt | 2 +- .../{ => classic}/AnnouncementsListPage.kt | 2 +- .../pages/{ => classic}/AssigneeListPage.kt | 2 +- .../{ => classic}/AssignmentDetailsPage.kt | 2 +- .../{ => classic}/AssignmentDueDatesPage.kt | 2 +- .../pages/{ => classic}/CommentLibraryPage.kt | 2 +- .../pages/{ => classic}/CourseBrowserPage.kt | 2 +- .../pages/{ => classic}/CourseSettingsPage.kt | 2 +- .../ui/pages/{ => classic}/DashboardPage.kt | 2 +- .../{ => classic}/DiscussionsDetailsPage.kt | 2 +- .../{ => classic}/DiscussionsListPage.kt | 2 +- .../EditAnnouncementDetailsPage.kt | 2 +- .../EditAssignmentDetailsPage.kt | 2 +- .../pages/{ => classic}/EditDashboardPage.kt | 2 +- .../EditDiscussionsDetailsPage.kt | 2 +- .../{ => classic}/EditPageDetailsPage.kt | 2 +- .../{ => classic}/EditProfileSettingsPage.kt | 2 +- .../{ => classic}/EditQuizDetailsPage.kt | 2 +- .../pages/{ => classic}/EditSyllabusPage.kt | 2 +- .../ui/pages/{ => classic}/FileListPage.kt | 2 +- .../ui/pages/{ => classic}/HelpPage.kt | 2 +- .../LeftSideNavigationDrawerPage.kt | 2 +- .../ui/pages/{ => classic}/ModulesPage.kt | 2 +- .../ui/pages/{ => classic}/NavDrawerPage.kt | 2 +- .../ui/pages/{ => classic}/NotATeacherPage.kt | 2 +- .../ui/pages/{ => classic}/PageListPage.kt | 2 +- .../ui/pages/{ => classic}/PeopleListPage.kt | 2 +- .../pages/{ => classic}/PersonContextPage.kt | 2 +- .../pages/{ => classic}/PostSettingsPage.kt | 2 +- .../{ => classic}/ProfileSettingsPage.kt | 2 +- .../{ => classic}/PushNotificationsPage.kt | 2 +- .../ui/pages/{ => classic}/QuizDetailsPage.kt | 2 +- .../ui/pages/{ => classic}/QuizListPage.kt | 2 +- .../{ => classic}/RemoteConfigSettingsPage.kt | 2 +- .../{ => classic}/SpeedGraderCommentsPage.kt | 2 +- .../SpeedGraderQuizSubmissionPage.kt | 2 +- .../pages/{ => classic}/StudentContextPage.kt | 3 +- .../ui/pages/{ => classic}/SyllabusPage.kt | 2 +- .../ui/pages/{ => classic}/TodoPage.kt | 2 +- .../UpdateFilePermissionsPage.kt | 2 +- .../pages/{ => classic}/WebViewLoginPage.kt | 2 +- .../AssignmentSubmissionListPage.kt | 2 +- .../ui/pages/{ => compose}/ProgressPage.kt | 2 +- .../ui/pages/compose/SpeedGraderGradePage.kt | 2 +- .../ui/pages/compose/SpeedGraderPage.kt | 2 +- .../EditSyllabusRenderTest.kt | 4 +- .../ModuleListRenderTest.kt | 4 +- .../PostGradeRenderTest.kt | 4 +- .../SyllabusRenderTest.kt | 6 +- .../renderpages}/EditSyllabusRenderPage.kt | 2 +- .../renderpages}/ModuleListRenderPage.kt | 6 +- .../renderpages}/PostGradeRenderPage.kt | 2 +- .../renderpages}/SyllabusRenderPage.kt | 4 +- .../instructure/teacher/ui/utils/Matchers.kt | 72 ---- .../teacher/ui/utils/TeacherComposeTest.kt | 4 +- .../teacher/ui/utils/TeacherTest.kt | 82 ++--- .../ui/utils/ViewInteractionDelegates.kt | 36 -- .../PageInteractionExtensions.kt} | 2 +- .../{ => extensions}/TeacherTestExtensions.kt | 3 +- .../WebInteractionExtensions.kt | 18 +- .../canvas/espresso/CanvasRunner.kt | 2 +- .../instructure/canvas/espresso/CanvasTest.kt | 18 +- .../canvas/espresso/CustomActions.kt | 27 ++ .../canvas/espresso/CustomMatchers.kt | 196 ++++++++++ .../canvas/espresso/TestAppManager.kt | 2 +- .../{ => annotations}/E2EAnnotation.kt | 2 +- .../{ => annotations}/FlakyE2EAnnotation.kt | 2 +- .../espresso/{ => annotations}/KnownBug.kt | 2 +- .../espresso/{ => annotations}/OfflineE2E.kt | 2 +- .../{ => annotations}/ReleaseExclude.kt | 2 +- .../{ => annotations}/RunsOnTablet.kt | 2 +- .../{ => annotations}/StubAnnotation.kt | 2 +- .../StubCoverageAnnotation.kt | 2 +- .../StubLandscapeAnnotation.kt | 2 +- .../{ => annotations}/StubMultiAPILevel.kt | 2 +- .../{ => annotations}/StubTabletAnnotation.kt | 2 +- .../interaction/CalendarInteractionTest.kt | 17 +- .../CreateUpdateEventInteractionTest.kt | 4 +- .../CreateUpdateToDoInteractionTest.kt | 4 +- .../EventDetailsInteractionTest.kt | 4 +- .../interaction/GradesInteractionTest.kt | 8 +- .../InboxComposeInteractionTest.kt | 4 +- .../InboxDetailsInteractionTest.kt | 4 +- .../interaction/InboxListInteractionTest.kt | 10 +- .../InboxSignatureInteractionTest.kt | 2 +- .../interaction/SettingsInteractionTest.kt | 2 +- .../interaction/SmartSearchInteractionTest.kt | 8 +- .../interaction/ToDoDetailsInteractionTest.kt | 4 +- ...inderPage.kt => AssignmentReminderPage.kt} | 2 +- .../common/pages/compose/GradesPage.kt | 2 +- .../{mockCanvas => mockcanvas}/Endpoint.kt | 12 +- .../HttpResponder.kt | 10 +- .../{mockCanvas => mockcanvas}/MockCanvas.kt | 4 +- .../MockCanvasInterceptor.kt | 6 +- .../endpoints/AccountEndpoints.kt | 22 +- .../endpoints/ApiEndpoint.kt | 32 +- .../endpoints/AssignmentEndpoints.kt | 15 +- .../endpoints/CanvadocEndpoints.kt | 13 +- .../endpoints/ConversationEndpoints.kt | 17 +- .../endpoints/CourseEndpoints.kt | 44 +-- .../endpoints/CoursepermissionsEndpoint.kt | 8 +- .../endpoints/EnrollmentEndpoint.kt | 13 +- .../endpoints/ExternalToolsEndpoints.kt | 9 +- .../endpoints/FileEndpoints.kt | 31 +- .../endpoints/FolderEndpoints.kt | 21 +- .../endpoints/MiscEndpoints.kt | 9 +- .../endpoints/OAuthEndpoint.kt | 4 +- .../endpoints/ObserverAlertsEndpoint.kt | 14 +- .../endpoints/RootEndpoint.kt | 14 +- .../endpoints/SearchEndpoint.kt | 12 +- .../endpoints/SubmissionEndpoints.kt | 16 +- .../endpoints/UserEndpoints.kt | 30 +- .../fakes/FakeAssignmentDetailsManager.kt | 4 +- .../fakes/FakeCommentLibraryManager.kt | 4 +- .../fakes/FakeCustomGradeStatusesManager.kt | 2 +- .../fakes/FakeInboxSettingsManager.kt | 4 +- .../fakes/FakePostPolicyManager.kt | 2 +- .../fakes/FakeStudentContextManager.kt | 4 +- .../fakes/FakeSubmissionCommentsManager.kt | 2 +- .../fakes/FakeSubmissionContentManager.kt | 4 +- .../fakes/FakeSubmissionDetailsManager.kt | 4 +- .../fakes/FakeSubmissionGradeManager.kt | 4 +- .../fakes/FakeSubmissionRubricManager.kt | 4 +- .../utils/AuthUtils.kt | 2 +- .../utils/PathUtils.kt | 4 +- .../utils/Randomizer.kt | 5 +- .../utils/RequestUtils.kt | 9 +- .../ComposeCustomMatchers.kt | 2 +- .../com/instructure/espresso/TestingUtils.kt | 48 ++- .../com/instructure/espresso/ViewUtils.kt | 34 -- .../com/instructure/espresso/filters/P0.kt | 25 -- .../com/instructure/espresso/filters/P1.kt | 25 -- .../com/instructure/espresso/filters/P2.kt | 25 -- .../com/instructure/espresso/filters/P3.kt | 25 -- .../com/instructure/espresso/filters/P4.kt | 25 -- libs/pandautils/flank.yml | 2 +- .../grades/GradesAssignmentItemTest.kt | 2 +- .../features/grades/GradesScreenTest.kt | 2 +- .../smartsearch/SmartSearchScreenTest.kt | 2 +- 497 files changed, 2575 insertions(+), 2858 deletions(-) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => classic}/HelpMenuE2ETest.kt (95%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/AlertsE2ETest.kt (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/AssignmentDetailsE2ETest.kt (95%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/AssignmentReminderE2ETest.kt (78%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/CalendarE2ETest.kt (99%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/CourseDetailsFrontPageE2ETest.kt (94%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/CourseDetailsSummaryE2ETest.kt (96%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/CourseDetailsSyllabusE2ETest.kt (94%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/CourseListE2ETest.kt (96%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/CreateAccountE2ETest.kt (96%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/DashboardE2ETest.kt (96%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/GradesListE2ETest.kt (97%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/InboxE2ETest.kt (99%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/LoginE2ETest.kt (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/ManageStudentsE2ETest.kt (96%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/{ => compose}/SettingsE2ETest.kt (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => classic}/DashboardPage.kt (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => classic}/FrontPagePage.kt (96%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => classic}/HelpPage.kt (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => classic}/LeftSideNavigationDrawerPage.kt (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/AddStudentBottomPage.kt (97%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/AlertsPage.kt (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/AnnouncementDetailsPage.kt (97%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/CourseDetailsPage.kt (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/CoursesPage.kt (97%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/CreateAccountPage.kt (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/ManageStudentsPage.kt (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/PairingCodePage.kt (96%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/ParentInboxCoursePickerPage.kt (95%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/QrPairingPage.kt (95%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/StudentAlertSettingsPage.kt (99%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/SummaryPage.kt (97%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/{ => compose}/SyllabusPage.kt (96%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/{compose/alerts/details/AnnouncementDetailsScreenTest.kt => rendertests/alerts/details/AnnouncementDetailsRenderTest.kt} (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/{compose/alerts/list/AlertsListItemTest.kt => rendertests/alerts/list/AlertsListItemRenderTest.kt} (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/{compose/alerts/list/AlertsScreenTest.kt => rendertests/alerts/list/AlertsRenderTest.kt} (99%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/{compose/alerts/settings/AlertSettingsScreenTest.kt => rendertests/alerts/settings/AlertSettingsRenderTest.kt} (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/{compose/courses/details/CourseDetailsScreenTest.kt => rendertests/courses/details/CourseDetailsRenderTest.kt} (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/{compose/courses/details/frontpage/FrontPageScreenTest.kt => rendertests/courses/details/frontpage/FrontPageRenderTest.kt} (96%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/{compose/courses/details/summary/SummaryScreenTest.kt => rendertests/courses/details/summary/SummaryRenderTest.kt} (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/{compose/courses/list/CoursesScreenTest.kt => rendertests/courses/list/CoursesRenderTest.kt} (97%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/{compose/login/createaccount/CreateAccountScreenTest.kt => rendertests/login/createaccount/CreateAccountRenderTest.kt} (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/{compose/managestudents/ManageStudentsScreenTest.kt => rendertests/managestudents/ManageStudentsRenderTest.kt} (98%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/ui/{compose/notaparent/NotAParentScreenTest.kt => rendertests/notaparent/NotAParentRenderTest.kt} (97%) rename apps/parent/src/androidTest/java/com/instructure/parentapp/utils/{ => extensions}/ParentTestExtensions.kt (96%) create mode 100644 apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt delete mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/LoginFindSchoolPageTest.kt delete mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/LoginLandingPageTest.kt delete mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/LoginSignInPageTest.kt rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/AnnouncementsE2ETest.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/CollaborationsE2ETest.kt (86%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/ConferencesE2ETest.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/DashboardE2ETest.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/DiscussionsE2ETest.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/FilesE2ETest.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/GradesE2ETest.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/HelpMenuE2ETest.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/LoginE2ETest.kt (92%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/ModulesE2ETest.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/NotificationsE2ETest.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/PagesE2ETest.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/PushNotificationsE2ETest.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/QuizzesE2ETest.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/ShareExtensionE2ETest.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/SyllabusE2ETest.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/TodoE2ETest.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/k5/GradesElementaryE2ETest.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/k5/ImportantDatesE2ETest.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/k5/ScheduleE2ETest.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/ManageOfflineContentE2ETest.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineAllCoursesE2ETest.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineAnnouncementsE2ETest.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineAssignmentsE2ETest.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineConferencesE2ETest.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineCourseBrowserE2ETest.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineDashboardE2ETest.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineDiscussionsE2ETest.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineFilesE2ETest.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineGradesE2ETest.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineLeftSideMenuE2ETest.kt (89%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineLoginE2ETest.kt (93%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineModulesE2ETest.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflinePagesE2ETest.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflinePeopleE2ETest.kt (92%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineSettingsE2ETest.kt (88%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineSyllabusE2ETest.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineSyncProgressE2ETest.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/offline/OfflineSyncSettingsE2ETest.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => classic}/usergroups/UserGroupFilesE2ETest.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => compose}/AssignmentsE2ETest.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => compose}/BookmarksE2ETest.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => compose}/CourseBrowserE2ETest.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => compose}/InboxE2ETest.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => compose}/PeopleE2ETest.kt (93%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => compose}/SettingsE2ETest.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => compose}/k5/HomeroomE2ETest.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/{ => compose}/k5/ResourcesE2ETest.kt (92%) delete mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/AllCoursesPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/AnnotationCommentListPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/AnnouncementListPage.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/BookmarkPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/CanvasWebViewPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/CollaborationsPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/ConferenceDetailsPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/ConferenceListPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/ConferencesPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/CourseBrowserPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/CourseGradesPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/DashboardPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/DiscussionDetailsPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/DiscussionListPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/FileChooserPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/FileListPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/GoToQuizPage.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/GradesPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/GroupBrowserPage.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/HelpPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/LeftSideNavigationDrawerPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/ModuleProgressionPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/ModulesPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/NotificationPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/PageDetailsPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/PageListPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/PairObserverPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/PandaAvatarPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/PeopleListPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/PersonDetailsPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/PickerSubmissionUploadPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/ProfileSettingsPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/PushNotificationsPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/QRLoginPage.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/QuizListPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/QuizTakingPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/RemoteConfigSettingsPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/ShareExtensionStatusPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/ShareExtensionTargetPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/StudentAssignmentDetailsPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/SubmissionDetailsEmptyContentPage.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/SubmissionDetailsPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/SyllabusPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/TextSubmissionUploadPage.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/TodoPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/UrlSubmissionUploadPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic/k5}/ElementaryCoursePage.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic/k5}/ElementaryDashboardPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic/k5}/HomeroomPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic/k5}/ImportantDatesPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic/k5}/ResourcesPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic/k5}/SchedulePage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/offline/ManageOfflineContentPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/offline/NativeDiscussionDetailsPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/offline/OfflineSyncSettingsPage.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{ => classic}/offline/SyncProgressPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/ConferenceDetailsRenderTest.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/ConferenceListRenderTest.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/DiscussionSubmissionViewRenderTest.kt (91%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/MediaSubmissionViewRenderTest.kt (92%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/PairObserverRenderTest.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/PickerSubmissionUploadRenderTest.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/QuizSubmissionViewRenderTest.kt (90%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/SubmissionCommentsRenderTest.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/SubmissionDetailsEmptyContentRenderTest.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/SubmissionDetailsRenderTest.kt (98%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/SubmissionFilesRenderTest.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/SubmissionRubricRenderTest.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/SyllabusRenderTest.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/TextSubmissionUploadRenderTest.kt (92%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/TextSubmissionViewRenderTest.kt (89%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/UploadStatusSubmissionRenderTest.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/UrlSubmissionUploadRenderTest.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests}/UrlSubmissionViewRenderTest.kt (93%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/ConferenceDetailsRenderPage.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/ConferenceListRenderPage.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/DiscussionSubmissionViewRenderPage.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/MediaSubmissionViewRenderPage.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/PairObserverRenderPage.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/PickerSubmissionUploadRenderPage.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/QuizSubmissionViewRenderPage.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/SubmissionCommentsRenderPage.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/SubmissionDetailsEmptyContentRenderPage.kt (92%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/SubmissionDetailsRenderPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/SubmissionFilesRenderPage.kt (95%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/SubmissionRubricRenderPage.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/SyllabusRenderPage.kt (96%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/TextSubmissionUploadRenderPage.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/TextSubmissionViewRenderPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/UploadStatusSubmissionViewRenderPage.kt (97%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/UrlSubmissionUploadRenderPage.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{pages/renderPages => rendertests/renderpages}/UrlSubmissionViewRenderPage.kt (94%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{renderTests => rendertests/renderpages}/views/GradeCellRenderTest.kt (98%) delete mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/utils/Matchers.kt rename apps/student/src/androidTest/java/com/instructure/student/{espresso => ui/utils}/StudentRenderTest.kt (67%) delete mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/utils/ViewUtils.kt rename apps/student/src/androidTest/java/com/instructure/student/ui/utils/{ => extensions}/StudentTestExtensions.kt (99%) rename apps/student/src/androidTest/java/com/instructure/student/ui/{e2e/offline/utils => utils/offline}/OfflineTestUtils.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/{ui => }/espresso/TeacherHiltTestApplication.kt (94%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/{ui => }/espresso/TeacherHiltTestRunner.kt (95%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/{ui => }/espresso/TestAppManager.kt (96%) delete mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginFindSchoolPageTest.kt delete mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginLandingPageTest.kt delete mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginSignInPageTest.kt rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/AnnouncementsE2ETest.kt (95%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/CommentLibraryE2ETest.kt (97%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/CourseSettingsE2ETest.kt (95%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/DashboardE2ETest.kt (96%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/DiscussionsE2ETest.kt (97%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/FilesE2ETest.kt (96%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/HelpMenuE2ETest.kt (94%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/LoginE2ETest.kt (97%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/ModulesE2ETest.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/PagesE2ETest.kt (96%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/PeopleE2ETest.kt (95%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/PushNotificationsE2ETest.kt (96%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/QuizE2ETest.kt (93%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/SettingsE2ETest.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/SyllabusE2ETest.kt (92%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => classic}/TodoE2ETest.kt (91%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => compose}/AssignmentE2ETest.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => compose}/InboxE2ETest.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/{ => compose}/SpeedGraderE2ETest.kt (95%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{AnnouncementsListPageTest.kt => interaction/AnnouncementsListInteractionTest.kt} (90%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{AssigneeListPageTest.kt => interaction/AssigneeListInteractionTest.kt} (89%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{AssignmentDetailsPageTest.kt => interaction/AssignmentDetailsInteractionTest.kt} (92%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{AssignmentDueDatesPageTest.kt => interaction/AssignmentDueDatesInteractionTest.kt} (87%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{AssignmentSubmissionListPageTest.kt => interaction/AssignmentSubmissionListInteractionTest.kt} (90%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{CommentLibraryPageTest.kt => interaction/CommentLibraryInteractionTest.kt} (92%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{CourseBrowserPageTest.kt => interaction/CourseBrowserInteractionTest.kt} (81%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{CourseSettingsPageTest.kt => interaction/CourseSettingsInteractionTest.kt} (87%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{DashboardPageTest.kt => interaction/DashboardInteractionTest.kt} (90%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{DiscussionsListPageTest.kt => interaction/DiscussionsListInteractionTest.kt} (87%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{EditAssignmentDetailsPageTest.kt => interaction/EditAssignmentDetailsInteractionTest.kt} (94%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{EditDashboardPageTest.kt => interaction/EditDashboardInteractionTest.kt} (89%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{EditQuizDetailsPageTest.kt => interaction/EditQuizDetailsInteractionTest.kt} (94%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{EditSyllabusPageTest.kt => interaction/EditSyllabusInteractionTest.kt} (87%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{InAppUpdatePageTest.kt => interaction/InAppUpdateInteractionTest.kt} (97%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{ModuleListPageTest.kt => interaction/ModuleListInteractionTest.kt} (96%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{NavDrawerPageTest.kt => interaction/NavDrawerInteractionTest.kt} (81%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{NotATeacherPageTest.kt => interaction/NotATeacherInteractionTest.kt} (82%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{PageListPageTest.kt => interaction/PageListInteractionTest.kt} (86%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{PersonContextPageTest.kt => interaction/PersonContextInteractionTest.kt} (83%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{QuizDetailsPageTest.kt => interaction/QuizDetailsInteractionTest.kt} (89%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{QuizListPageTest.kt => interaction/QuizListInteractionTest.kt} (87%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{QuizSubmissionListPageTest.kt => interaction/QuizSubmissionListInteractionTest.kt} (90%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{SpeedGraderCommentsPageTest.kt => interaction/SpeedGraderCommentsInteractionTest.kt} (93%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{SpeedGraderFilesPageTest.kt => interaction/SpeedGraderFilesInteractionTest.kt} (82%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{SpeedGraderGradePageTest.kt => interaction/SpeedGraderGradeInteractionTest.kt} (91%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{SpeedGraderPageTest.kt => interaction/SpeedGraderInteractionTest.kt} (87%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{SpeedGraderQuizSubmissionPageTest.kt => interaction/SpeedGraderQuizSubmissionInteractionTest.kt} (85%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{SyllabusPageTest.kt => interaction/SyllabusInteractionTest.kt} (89%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{TeacherCalendarPageTest.kt => interaction/TeacherCalendarInteractionTest.kt} (83%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{TeacherCalendarToDoDetailsPageTest.kt => interaction/TeacherCalendarToDoDetailsInteractionTest.kt} (84%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{TeacherCreateUpdateEventPageTest.kt => interaction/TeacherCreateUpdateEventInteractionTest.kt} (86%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{ => interaction}/TeacherCreateUpdateToDoInteractionTest.kt (89%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{TeacherEventDetailsPageTest.kt => interaction/TeacherEventDetailsInteractionTest.kt} (82%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{ => interaction}/TeacherInboxComposeInteractionTest.kt (85%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{TeacherInboxListPageTest.kt => interaction/TeacherInboxListInteractionTest.kt} (80%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{ => interaction}/TeacherInboxSignatureInteractionTest.kt (79%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{UpdateFilePermissionsPageTest.kt => interaction/UpdateFilePermissionsInteractionTest.kt} (92%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/AllCoursesListPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/AnnouncementsListPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/AssigneeListPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/AssignmentDetailsPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/AssignmentDueDatesPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/CommentLibraryPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/CourseBrowserPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/CourseSettingsPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/DashboardPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/DiscussionsDetailsPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/DiscussionsListPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/EditAnnouncementDetailsPage.kt (97%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/EditAssignmentDetailsPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/EditDashboardPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/EditDiscussionsDetailsPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/EditPageDetailsPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/EditProfileSettingsPage.kt (97%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/EditQuizDetailsPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/EditSyllabusPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/FileListPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/HelpPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/LeftSideNavigationDrawerPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/ModulesPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/NavDrawerPage.kt (96%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/NotATeacherPage.kt (97%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/PageListPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/PeopleListPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/PersonContextPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/PostSettingsPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/ProfileSettingsPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/PushNotificationsPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/QuizDetailsPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/QuizListPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/RemoteConfigSettingsPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/SpeedGraderCommentsPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/SpeedGraderQuizSubmissionPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/StudentContextPage.kt (97%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/SyllabusPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/TodoPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/UpdateFilePermissionsPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => classic}/WebViewLoginPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => compose}/AssignmentSubmissionListPage.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/{ => compose}/ProgressPage.kt (97%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{renderTests => rendertests}/EditSyllabusRenderTest.kt (95%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{renderTests => rendertests}/ModuleListRenderTest.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{renderTests => rendertests}/PostGradeRenderTest.kt (97%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{renderTests => rendertests}/SyllabusRenderTest.kt (96%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{renderTests/pages => rendertests/renderpages}/EditSyllabusRenderPage.kt (98%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{renderTests/pages => rendertests/renderpages}/ModuleListRenderPage.kt (95%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{renderTests/pages => rendertests/renderpages}/PostGradeRenderPage.kt (97%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/{renderTests/pages => rendertests/renderpages}/SyllabusRenderPage.kt (95%) delete mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/Matchers.kt delete mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/ViewInteractionDelegates.kt rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/{PageExtensions.kt => extensions/PageInteractionExtensions.kt} (96%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/{ => extensions}/TeacherTestExtensions.kt (99%) rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/{ => extensions}/WebInteractionExtensions.kt (71%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{ => annotations}/E2EAnnotation.kt (80%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{ => annotations}/FlakyE2EAnnotation.kt (94%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{ => annotations}/KnownBug.kt (94%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{ => annotations}/OfflineE2E.kt (94%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{ => annotations}/ReleaseExclude.kt (94%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{ => annotations}/RunsOnTablet.kt (94%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{ => annotations}/StubAnnotation.kt (81%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{ => annotations}/StubCoverageAnnotation.kt (94%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{ => annotations}/StubLandscapeAnnotation.kt (94%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{ => annotations}/StubMultiAPILevel.kt (94%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{ => annotations}/StubTabletAnnotation.kt (93%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/{ReminderPage.kt => AssignmentReminderPage.kt} (98%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/Endpoint.kt (91%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/HttpResponder.kt (88%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/MockCanvas.kt (99%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/MockCanvasInterceptor.kt (88%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/AccountEndpoints.kt (90%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/ApiEndpoint.kt (96%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/AssignmentEndpoints.kt (94%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/CanvadocEndpoints.kt (83%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/ConversationEndpoints.kt (94%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/CourseEndpoints.kt (97%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/CoursepermissionsEndpoint.kt (81%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/EnrollmentEndpoint.kt (82%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/ExternalToolsEndpoints.kt (78%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/FileEndpoints.kt (65%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/FolderEndpoints.kt (80%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/MiscEndpoints.kt (85%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/OAuthEndpoint.kt (86%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/ObserverAlertsEndpoint.kt (82%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/RootEndpoint.kt (86%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/SearchEndpoint.kt (88%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/SubmissionEndpoints.kt (95%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/endpoints/UserEndpoints.kt (95%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/fakes/FakeAssignmentDetailsManager.kt (92%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/fakes/FakeCommentLibraryManager.kt (89%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/fakes/FakeCustomGradeStatusesManager.kt (96%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/fakes/FakeInboxSettingsManager.kt (92%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/fakes/FakePostPolicyManager.kt (96%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/fakes/FakeStudentContextManager.kt (97%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/fakes/FakeSubmissionCommentsManager.kt (96%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/fakes/FakeSubmissionContentManager.kt (97%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/fakes/FakeSubmissionDetailsManager.kt (94%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/fakes/FakeSubmissionGradeManager.kt (97%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/fakes/FakeSubmissionRubricManager.kt (96%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/utils/AuthUtils.kt (96%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/utils/PathUtils.kt (97%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/utils/Randomizer.kt (97%) rename automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/{mockCanvas => mockcanvas}/utils/RequestUtils.kt (97%) rename automation/espresso/src/main/kotlin/com/instructure/{composeTest => composetest}/ComposeCustomMatchers.kt (97%) delete mode 100644 automation/espresso/src/main/kotlin/com/instructure/espresso/ViewUtils.kt delete mode 100644 automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P0.kt delete mode 100644 automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P1.kt delete mode 100644 automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P2.kt delete mode 100644 automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P3.kt delete mode 100644 automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P4.kt diff --git a/apps/parent/flank.yml b/apps/parent/flank.yml index bf30186866..8680905867 100644 --- a/apps/parent/flank.yml +++ b/apps/parent/flank.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/parent/flank_coverage.yml b/apps/parent/flank_coverage.yml index 07643bce00..8f8f689d5c 100644 --- a/apps/parent/flank_coverage.yml +++ b/apps/parent/flank_coverage.yml @@ -19,7 +19,7 @@ gcloud: directories-to-pull: - /sdcard/ test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.OfflineE2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.OfflineE2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubCoverage device: - model: Pixel2.arm version: 29 diff --git a/apps/parent/flank_e2e.yml b/apps/parent/flank_e2e.yml index a24358fd42..d82786e4a1 100644 --- a/apps/parent/flank_e2e.yml +++ b/apps/parent/flank_e2e.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/parent/flank_landscape.yml b/apps/parent/flank_landscape.yml index 2369cddb2b..128342d795 100644 --- a/apps/parent/flank_landscape.yml +++ b/apps/parent/flank_landscape.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubLandscape, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubLandscape, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/parent/flank_multi_api_level.yml b/apps/parent/flank_multi_api_level.yml index 4213e759ef..c91e79f4e8 100644 --- a/apps/parent/flank_multi_api_level.yml +++ b/apps/parent/flank_multi_api_level.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubMultiAPILevel, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: NexusLowRes version: 27 diff --git a/apps/parent/flank_tablet.yml b/apps/parent/flank_tablet.yml index 8beaae55bc..456bf4d0c4 100644 --- a/apps/parent/flank_tablet.yml +++ b/apps/parent/flank_tablet.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubTablet, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubTablet, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: MediumTablet.arm version: 29 diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/HelpMenuE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/classic/HelpMenuE2ETest.kt similarity index 95% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/HelpMenuE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/classic/HelpMenuE2ETest.kt index 5779f4e478..c81a7d5432 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/HelpMenuE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/classic/HelpMenuE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.classic import android.util.Log import androidx.test.espresso.intent.Intents -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText import com.instructure.parentapp.R import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AlertsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AlertsE2ETest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AlertsE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AlertsE2ETest.kt index 5704eeac46..862af1d795 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AlertsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AlertsE2ETest.kt @@ -13,15 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.models.AlertType import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi @@ -30,10 +31,9 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -74,7 +74,7 @@ class AlertsE2ETest : ParentComposeTest() { studentAlertSettingsPage.setThreshold(AlertType.ASSIGNMENT_GRADE_HIGH, "80") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Alerts Page.") dashboardPage.clickAlertsBottomMenu() @@ -133,7 +133,7 @@ class AlertsE2ETest : ParentComposeTest() { Thread.sleep(5000) // Allow the grading to propagate Log.d(STEP_TAG, "Navigate back to Alerts Page and refresh it.") - ViewUtils.pressBackButton(2) + pressBackButton(2) alertsPage.refresh() Log.d(ASSERTION_TAG, "Assert that the 'Assignment Grade Below 20' alert is displayed.") @@ -180,7 +180,7 @@ class AlertsE2ETest : ParentComposeTest() { studentAlertSettingsPage.setThreshold(AlertType.ASSIGNMENT_GRADE_HIGH, "80") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Alerts Page.") dashboardPage.clickAlertsBottomMenu() @@ -298,7 +298,7 @@ class AlertsE2ETest : ParentComposeTest() { studentAlertSettingsPage.assertPercentageThreshold(AlertType.COURSE_GRADE_LOW, "Never") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Alerts Page.") dashboardPage.clickAlertsBottomMenu() @@ -377,7 +377,7 @@ class AlertsE2ETest : ParentComposeTest() { studentAlertSettingsPage.assertPercentageThreshold(AlertType.COURSE_GRADE_HIGH, "80%") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Alerts Page.") dashboardPage.clickAlertsBottomMenu() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentDetailsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentDetailsE2ETest.kt similarity index 95% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentDetailsE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentDetailsE2ETest.kt index cc46985157..44392f3d72 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentDetailsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentDetailsE2ETest.kt @@ -14,15 +14,16 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.GradingType @@ -30,11 +31,10 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -113,7 +113,7 @@ class AssignmentDetailsE2ETest : ParentComposeTest() { assignmentDetailsPage.assertSelectedAttempt(1) Log.d(STEP_TAG, "Navigate back to the course list page of the selected student.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Select the other student, '${student2.name}', who does not have any grade (and submission) for the given assignment and select this student.") dashboardPage.openStudentSelector() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentReminderE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt similarity index 78% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentReminderE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt index 25fb89b8f3..c065eb0492 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentReminderE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt @@ -14,16 +14,16 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType @@ -36,8 +36,8 @@ import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.pandautils.utils.toFormattedString import com.instructure.parentapp.R import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.util.Calendar @@ -92,71 +92,71 @@ class AssignmentReminderE2ETest: ParentComposeTest() { assignmentDetailsPage.assertPageObjects() Log.d(ASSERTION_TAG, "Assert that the reminder section is displayed as well.") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneHour = futureDueDate.apply { add(Calendar.HOUR, -1) } Log.d(STEP_TAG, "Select '1 Hour Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneHour) - reminderPage.selectTime(reminderDateOneHour) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneHour) + assignmentReminderPage.selectTime(reminderDateOneHour) Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Hour Before' again.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneHour) - reminderPage.selectTime(reminderDateOneHour) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneHour) + assignmentReminderPage.selectTime(reminderDateOneHour) Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice.") checkToastText(R.string.reminderAlreadySet, activityRule.activity) Log.d(STEP_TAG, "Remove the '1 Hour Before' reminder, confirm the deletion dialog.") - reminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) Log.d(ASSERTION_TAG, "Assert that the '1 Hour Before' reminder is not displayed any more.") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) futureDueDate.apply { add(Calendar.HOUR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneWeek = futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, -1) } Log.d(STEP_TAG, "Select '1 Week Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneWeek) - reminderPage.selectTime(reminderDateOneWeek) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneWeek) + assignmentReminderPage.selectTime(reminderDateOneWeek) Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up a reminder which has already passed (for example cannot pick '1 Week Before' reminder for an assignment which ends tomorrow).") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) checkToastText(R.string.reminderInPast, activityRule.activity) futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) } Log.d(STEP_TAG, "Select '1 Day Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneDay) - reminderPage.selectTime(reminderDateOneDay) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneDay) + assignmentReminderPage.selectTime(reminderDateOneDay) Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Day Before' again.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneDay) - reminderPage.selectTime(reminderDateOneDay) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneDay) + assignmentReminderPage.selectTime(reminderDateOneDay) Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice. (Because 1 days and 24 hours is the same)") checkToastText(R.string.reminderAlreadySet, activityRule.activity) @@ -169,7 +169,7 @@ class AssignmentReminderE2ETest: ParentComposeTest() { courseDetailsPage.clickAssignment(alreadyPastAssignment.name) Log.d(ASSERTION_TAG, "Assert that the reminder section is NOT displayed, because the '${alreadyPastAssignment.name}' assignment has already passed..") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @E2E @@ -216,63 +216,63 @@ class AssignmentReminderE2ETest: ParentComposeTest() { assignmentDetailsPage.assertPageObjects() Log.d(ASSERTION_TAG, "Assert that the reminder section is displayed as well.") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneHour = futureDueDate.apply { add(Calendar.HOUR, -1) } Log.d(STEP_TAG, "Select '1 Hour Before'.") - reminderPage.clickBeforeReminderOption("1 Hour Before") + assignmentReminderPage.clickBeforeReminderOption("1 Hour Before") Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Hour Before' again.") - reminderPage.clickBeforeReminderOption("1 Hour Before") + assignmentReminderPage.clickBeforeReminderOption("1 Hour Before") Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice.") checkToastText(R.string.reminderAlreadySet, activityRule.activity) Log.d(STEP_TAG, "Remove the '1 Hour Before' reminder, confirm the deletion dialog.") - reminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) Log.d(ASSERTION_TAG, "Assert that the '1 Hour Before' reminder is not displayed any more.") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) futureDueDate.apply { add(Calendar.HOUR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneWeek = futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, -1) } Log.d(STEP_TAG, "Select '1 Week Before'.") - reminderPage.clickBeforeReminderOption("1 Week Before") + assignmentReminderPage.clickBeforeReminderOption("1 Week Before") Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up a reminder which has already passed (for example cannot pick '1 Week Before' reminder for an assignment which ends tomorrow).") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) composeTestRule.waitForIdle() checkToastText(R.string.reminderInPast, activityRule.activity) composeTestRule.waitForIdle() futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) } Log.d(STEP_TAG, "Select '1 Day Before'.") - reminderPage.clickBeforeReminderOption("1 Day Before") + assignmentReminderPage.clickBeforeReminderOption("1 Day Before") Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Day Before' again.") - reminderPage.clickBeforeReminderOption("1 Day Before") + assignmentReminderPage.clickBeforeReminderOption("1 Day Before") Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice. (Because 1 days and 24 hours is the same)") composeTestRule.waitForIdle() @@ -287,6 +287,6 @@ class AssignmentReminderE2ETest: ParentComposeTest() { courseDetailsPage.clickAssignment(alreadyPastAssignment.name) Log.d(ASSERTION_TAG, "Assert that the reminder section is NOT displayed, because the '${alreadyPastAssignment.name}' assignment has already passed..") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } } \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CalendarE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CalendarE2ETest.kt similarity index 99% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CalendarE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CalendarE2ETest.kt index 393e932c37..32f2c6da14 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CalendarE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CalendarE2ETest.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.api.CalendarEventApi @@ -32,8 +32,8 @@ import com.instructure.dataseeding.util.CanvasNetworkAdapter import com.instructure.espresso.getDateInCanvasCalendarFormat import com.instructure.pandautils.features.calendar.CalendarPrefs import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsFrontPageE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsFrontPageE2ETest.kt similarity index 94% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsFrontPageE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsFrontPageE2ETest.kt index d5f3df0884..9bf77bcd40 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsFrontPageE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsFrontPageE2ETest.kt @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.PagesApi @@ -31,8 +31,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSummaryE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSummaryE2ETest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSummaryE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSummaryE2ETest.kt index 63297e7354..d9d617e467 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSummaryE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSummaryE2ETest.kt @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.api.AssignmentsApi @@ -35,8 +35,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.util.Date diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSyllabusE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSyllabusE2ETest.kt similarity index 94% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSyllabusE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSyllabusE2ETest.kt index c052d4683c..3bc98c4b44 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSyllabusE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSyllabusE2ETest.kt @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.model.SubmissionType @@ -30,8 +30,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseListE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseListE2ETest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseListE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseListE2ETest.kt index 80972ffcda..3bcb8a413e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseListE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseListE2ETest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.GradingType @@ -31,8 +31,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CreateAccountE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CreateAccountE2ETest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CreateAccountE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CreateAccountE2ETest.kt index 3a2fb468fe..1868d2b426 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CreateAccountE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CreateAccountE2ETest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.app.Activity import android.app.Instrumentation @@ -25,15 +25,15 @@ import androidx.compose.ui.test.performClick import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intending import androidx.test.espresso.intent.matcher.IntentMatchers -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.UserApi import com.instructure.espresso.randomString import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData +import com.instructure.parentapp.utils.extensions.seedData import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.core.AllOf import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/DashboardE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/DashboardE2ETest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/DashboardE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/DashboardE2ETest.kt index 84914485b0..c0a61f6c0f 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/DashboardE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/DashboardE2ETest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.dataseeding.api.SubmissionsApi @@ -33,8 +33,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/GradesListE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/GradesListE2ETest.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/GradesListE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/GradesListE2ETest.kt index e2bb9b533c..043611177a 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/GradesListE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/GradesListE2ETest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.GradingType @@ -33,8 +33,8 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/InboxE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/InboxE2ETest.kt similarity index 99% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/InboxE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/InboxE2ETest.kt index 9c125bd0c8..cb486a2e8e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/InboxE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/InboxE2ETest.kt @@ -14,18 +14,18 @@ * along with this program. If not, see . * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.os.SystemClock.sleep import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.ReleaseExclude import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.ReleaseExclude import com.instructure.canvas.espresso.refresh import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.toApiString @@ -38,8 +38,8 @@ import com.instructure.dataseeding.model.UpdateCourse import com.instructure.dataseeding.util.CanvasNetworkAdapter import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.util.Date diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/LoginE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/LoginE2ETest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/LoginE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/LoginE2ETest.kt index 2ed14fe38d..fa2aa66627 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/LoginE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/LoginE2ETest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.dataseeding.api.UserApi import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.espresso.withIdlingResourceDisabled import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData +import com.instructure.parentapp.utils.extensions.seedData import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/ManageStudentsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/ManageStudentsE2ETest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/ManageStudentsE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/ManageStudentsE2ETest.kt index 98c0e27f17..af4b8e6e20 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/ManageStudentsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/ManageStudentsE2ETest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.dataseeding.api.SubmissionsApi @@ -33,8 +33,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/SettingsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/SettingsE2ETest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/SettingsE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/SettingsE2ETest.kt index cb4120f0fb..26e37c737c 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/SettingsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/SettingsE2ETest.kt @@ -14,17 +14,18 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType @@ -32,13 +33,12 @@ import com.instructure.dataseeding.util.CanvasNetworkAdapter import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.pandautils.utils.AppTheme import com.instructure.parentapp.R import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -228,7 +228,7 @@ class SettingsE2ETest : ParentComposeTest() { inboxSignatureSettingsPage.assertSignatureEnabledState(true) Log.d(STEP_TAG, "Navigate back to the Dashboard.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Left Side Navigation Drawer menu.") dashboardPage.openLeftSideMenu() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AddStudentInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AddStudentInteractionTest.kt index edebe75061..33b43480dd 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AddStudentInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AddStudentInteractionTest.kt @@ -25,12 +25,12 @@ import androidx.test.espresso.intent.matcher.IntentMatchers 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.addPairingCode -import com.instructure.canvas.espresso.mockCanvas.addStudent -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addPairingCode +import com.instructure.canvas.espresso.mockcanvas.addStudent +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.hamcrest.core.AllOf diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertSettingsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertSettingsInteractionTest.kt index 3d3c353509..b85ea55dda 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertSettingsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertSettingsInteractionTest.kt @@ -19,12 +19,12 @@ 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.addObserverAlertThreshold -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addObserverAlertThreshold +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.AlertType import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test 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 index 09200348ba..e8412bd28d 100644 --- 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 @@ -18,13 +18,13 @@ 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.addAssignmentsToGroups -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addObserverAlert -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addObserverAlert +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.AlertType @@ -32,7 +32,7 @@ import com.instructure.canvasapi2.models.AlertWorkflowState import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt index e6553daca7..e879991018 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -28,13 +28,13 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.checkToastText -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups -import com.instructure.canvas.espresso.mockCanvas.addObserverAlert -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups +import com.instructure.canvas.espresso.mockcanvas.addObserverAlert +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.AlertType @@ -45,7 +45,7 @@ import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.utils.toFormattedString import com.instructure.parentapp.R import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -269,7 +269,7 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @Test @@ -280,7 +280,7 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { val assignment = data.addAssignment(course.id, name = "Test Assignment") gotoAssignment(data, assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @Test @@ -293,7 +293,7 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @Test @@ -309,12 +309,12 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) } @Test @@ -330,17 +330,17 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) - reminderPage.removeReminderWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderCalendar.time.toFormattedString()) - reminderPage.assertReminderNotDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderCalendar.time.toFormattedString()) } @Test @@ -356,10 +356,10 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) checkToastText(R.string.reminderInPast, activityRule.activity) } @@ -377,15 +377,15 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) checkToastText(R.string.reminderAlreadySet, activityRule.activity) } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CourseDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CourseDetailsInteractionTest.kt index e284cd6181..488e38a3f3 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CourseDetailsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CourseDetailsInteractionTest.kt @@ -17,13 +17,13 @@ package com.instructure.parentapp.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Tab import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CoursesInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CoursesInteractionTest.kt index 186329b446..6c4492e6f1 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CoursesInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CoursesInteractionTest.kt @@ -17,13 +17,13 @@ package com.instructure.parentapp.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment -import com.instructure.canvas.espresso.mockCanvas.addEnrollment -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCourseWithEnrollment +import com.instructure.canvas.espresso.mockcanvas.addEnrollment +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Enrollment import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CreateAccountInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CreateAccountInteractionTest.kt index 14e1b2528e..cb0ba0790d 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CreateAccountInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CreateAccountInteractionTest.kt @@ -26,9 +26,9 @@ import androidx.compose.ui.test.performClick import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intending import androidx.test.espresso.intent.matcher.IntentMatchers -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addPairingCode -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addPairingCode +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.parentapp.utils.ParentComposeTest import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.core.AllOf diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt index b7cbab1033..d1e19afd22 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt @@ -25,13 +25,13 @@ 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.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.canvasapi2.utils.Pronouns import com.instructure.loginapi.login.R import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test 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 index b5f82a859e..3250a233d4 100644 --- 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 @@ -24,10 +24,10 @@ import androidx.compose.ui.test.onNodeWithText 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.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt index 69342cf8f5..2da7ba5fae 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt @@ -23,17 +23,17 @@ 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.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.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.CoreMatchers import org.junit.After 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 index 422e6385be..1e00fb8342 100644 --- 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 @@ -21,18 +21,18 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.CalendarInteractionTest import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.User import com.instructure.espresso.ModuleItemInteractions import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules 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 index 954aaf1deb..c5024f54e4 100644 --- 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 @@ -20,14 +20,14 @@ 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.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.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers 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 index 030aaea55b..c4b9a8a330 100644 --- 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 @@ -20,14 +20,14 @@ 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.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.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers 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 index 4a05c97fb6..17f0da5fbc 100644 --- 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 @@ -20,13 +20,13 @@ 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.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.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentGradesInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentGradesInteractionTest.kt index 031713a7e7..bc1fe7d0f0 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentGradesInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentGradesInteractionTest.kt @@ -18,17 +18,17 @@ package com.instructure.parentapp.ui.interaction import com.instructure.canvas.espresso.common.interaction.GradesInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.CoursesPage +import com.instructure.parentapp.ui.pages.compose.CoursesPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt index 64656a3b15..9c1b9bb7b1 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt @@ -7,19 +7,19 @@ import com.google.android.apps.common.testing.accessibility.framework.checks.Spe import com.instructure.canvas.espresso.common.interaction.InboxComposeInteractionTest import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.canvas.espresso.common.pages.compose.InboxComposePage -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.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +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.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -38,10 +38,10 @@ import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.type.EnrollmentType import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage -import com.instructure.parentapp.ui.pages.ParentInboxCoursePickerPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage +import com.instructure.parentapp.ui.pages.compose.ParentInboxCoursePickerPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxDetailsInteractionTest.kt index 7341fe4d02..7e981b4d6c 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxDetailsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxDetailsInteractionTest.kt @@ -21,18 +21,18 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.InboxDetailsInteractionTest import com.instructure.canvas.espresso.common.pages.InboxPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addConversation -import com.instructure.canvas.espresso.mockCanvas.addConversationWithMultipleMessages -import com.instructure.canvas.espresso.mockCanvas.addConversations -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addConversation +import com.instructure.canvas.espresso.mockcanvas.addConversationWithMultipleMessages +import com.instructure.canvas.espresso.mockcanvas.addConversations +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Conversation 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.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers 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 index 881840416d..b337e1f824 100644 --- 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 @@ -20,17 +20,17 @@ 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.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.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt index 65bc4e5d89..a28305365a 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt @@ -20,17 +20,17 @@ 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.InboxSignatureInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -43,10 +43,10 @@ import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.managers.graphql.SubmissionGradeManager import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage -import com.instructure.parentapp.ui.pages.LeftSideNavigationDrawerPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage +import com.instructure.parentapp.ui.pages.classic.LeftSideNavigationDrawerPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules 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 index 986ced8b8e..98715e73ef 100644 --- 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 @@ -20,14 +20,14 @@ 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.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.ui.pages.LeftSideNavigationDrawerPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage +import com.instructure.parentapp.ui.pages.classic.LeftSideNavigationDrawerPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers 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 index b809c6deba..1fd6ab164f 100644 --- 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 @@ -21,15 +21,15 @@ 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.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.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/SummaryInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/SummaryInteractionTest.kt index c52d93ce4b..838584bdd5 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/SummaryInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/SummaryInteractionTest.kt @@ -17,11 +17,11 @@ package com.instructure.parentapp.ui.interaction import androidx.test.core.app.ApplicationProvider -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Course @@ -31,7 +31,7 @@ import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.utils.getDisplayDate import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules 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/classic/DashboardPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/DashboardPage.kt index a8e830b5ab..75145bcd10 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/DashboardPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/FrontPagePage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/FrontPagePage.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/FrontPagePage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/FrontPagePage.kt index 51f7dff1de..10c3ec2cb0 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/FrontPagePage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/FrontPagePage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.web.assertion.WebViewAssertions diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/HelpPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/HelpPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/HelpPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/HelpPage.kt index 38a3e523fb..5fa254575a 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/HelpPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/HelpPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.classic import android.app.Instrumentation import android.content.Intent diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/LeftSideNavigationDrawerPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/LeftSideNavigationDrawerPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/LeftSideNavigationDrawerPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/LeftSideNavigationDrawerPage.kt index 315e61635f..4b337d12c8 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/LeftSideNavigationDrawerPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/LeftSideNavigationDrawerPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.action.ViewActions diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AddStudentBottomPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AddStudentBottomPage.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AddStudentBottomPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AddStudentBottomPage.kt index 4371785232..4102565424 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AddStudentBottomPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AddStudentBottomPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasAnyAncestor 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/compose/AlertsPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AlertsPage.kt index 09f206e617..53c2ecfed0 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AlertsPage.kt @@ -12,7 +12,7 @@ * 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 + */ package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AnnouncementDetailsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AnnouncementDetailsPage.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AnnouncementDetailsPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AnnouncementDetailsPage.kt index e148fe2763..1a6efe19a6 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AnnouncementDetailsPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AnnouncementDetailsPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CourseDetailsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CourseDetailsPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CourseDetailsPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CourseDetailsPage.kt index 9e8282981e..3df5436110 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CourseDetailsPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CourseDetailsPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.graphics.Color import androidx.compose.ui.test.assertIsDisplayed diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CoursesPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CoursesPage.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CoursesPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CoursesPage.kt index 82859ba87e..a47dea0eab 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CoursesPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CoursesPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.graphics.Color import androidx.compose.ui.test.assertIsDisplayed @@ -31,7 +31,7 @@ import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import com.instructure.canvasapi2.models.Course -import com.instructure.composeTest.hasSiblingWithText +import com.instructure.composetest.hasSiblingWithText import com.instructure.dataseeding.model.CourseApiModel import com.instructure.espresso.assertTextColor import com.instructure.pandares.R diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CreateAccountPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CreateAccountPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CreateAccountPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CreateAccountPage.kt index 0529291077..4ad1c207db 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CreateAccountPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CreateAccountPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.isDisplayed 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/compose/ManageStudentsPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/ManageStudentsPage.kt index 81e45aa617..95972ed009 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/ManageStudentsPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/PairingCodePage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/PairingCodePage.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/PairingCodePage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/PairingCodePage.kt index d717c245ea..61e273915f 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/PairingCodePage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/PairingCodePage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ParentInboxCoursePickerPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/ParentInboxCoursePickerPage.kt similarity index 95% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ParentInboxCoursePickerPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/ParentInboxCoursePickerPage.kt index 580e56bcad..4e938b98f5 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ParentInboxCoursePickerPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/ParentInboxCoursePickerPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.hasAnyChild import androidx.compose.ui.test.hasText diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/QrPairingPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/QrPairingPage.kt similarity index 95% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/QrPairingPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/QrPairingPage.kt index 884b62e381..62067478e5 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/QrPairingPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/QrPairingPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.junit4.ComposeTestRule diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/StudentAlertSettingsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/StudentAlertSettingsPage.kt similarity index 99% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/StudentAlertSettingsPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/StudentAlertSettingsPage.kt index cc632d4a88..677c7c4e7e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/StudentAlertSettingsPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/StudentAlertSettingsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SummaryPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SummaryPage.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SummaryPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SummaryPage.kt index a60f645392..18cbbe3bcc 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SummaryPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SummaryPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasAnySibling diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SyllabusPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SyllabusPage.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SyllabusPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SyllabusPage.kt index 2a47f16254..c0a1bcfdc2 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SyllabusPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SyllabusPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.web.assertion.WebViewAssertions diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/details/AnnouncementDetailsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/details/AnnouncementDetailsRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/details/AnnouncementDetailsScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/details/AnnouncementDetailsRenderTest.kt index 6d21d0088b..33a21f486d 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/details/AnnouncementDetailsScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/details/AnnouncementDetailsRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.alerts.details +package com.instructure.parentapp.ui.rendertests.alerts.details import android.graphics.Color import androidx.compose.ui.test.assertHasClickAction @@ -37,7 +37,7 @@ import java.time.Instant import java.util.Date @RunWith(AndroidJUnit4::class) -class AnnouncementDetailsScreenTest { +class AnnouncementDetailsRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsListItemTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsListItemRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsListItemTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsListItemRenderTest.kt index 6c2f3e509c..1736d40d68 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsListItemTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsListItemRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.compose.alerts.list +package com.instructure.parentapp.ui.rendertests.alerts.list import android.graphics.Color import androidx.compose.ui.test.assertHasClickAction @@ -24,7 +24,7 @@ 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.composeTest.hasDrawable +import com.instructure.composetest.hasDrawable import com.instructure.parentapp.R import com.instructure.parentapp.features.alerts.list.AlertsItemUiState import com.instructure.parentapp.features.alerts.list.AlertsListItem @@ -36,7 +36,7 @@ import java.util.Date import java.util.Locale @RunWith(AndroidJUnit4::class) -class AlertsListItemTest { +class AlertsListItemRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsRenderTest.kt similarity index 99% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsRenderTest.kt index bf9df1d50b..78d5f75d25 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.compose.alerts.list +package com.instructure.parentapp.ui.rendertests.alerts.list import android.graphics.Color import androidx.compose.material.ExperimentalMaterialApi @@ -41,7 +41,7 @@ import java.util.Locale @ExperimentalMaterialApi @RunWith(AndroidJUnit4::class) -class AlertsScreenTest { +class AlertsRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/settings/AlertSettingsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/settings/AlertSettingsRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/settings/AlertSettingsScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/settings/AlertSettingsRenderTest.kt index c104183749..c54236114c 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/settings/AlertSettingsScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/settings/AlertSettingsRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.compose.alerts.settings +package com.instructure.parentapp.ui.rendertests.alerts.settings import android.graphics.Color import androidx.compose.ui.test.assertHasClickAction @@ -29,13 +29,13 @@ import com.instructure.canvasapi2.models.ThresholdWorkflowState import com.instructure.canvasapi2.models.User import com.instructure.parentapp.features.alerts.settings.AlertSettingsScreen import com.instructure.parentapp.features.alerts.settings.AlertSettingsUiState -import com.instructure.parentapp.ui.pages.StudentAlertSettingsPage +import com.instructure.parentapp.ui.pages.compose.StudentAlertSettingsPage import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class AlertSettingsScreenTest { +class AlertSettingsRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/CourseDetailsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/CourseDetailsRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/CourseDetailsScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/CourseDetailsRenderTest.kt index 0be595af88..679b0ff753 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/CourseDetailsScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/CourseDetailsRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.courses.details +package com.instructure.parentapp.ui.rendertests.courses.details import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed @@ -39,7 +39,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class CourseDetailsScreenTest { +class CourseDetailsRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/frontpage/FrontPageScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/frontpage/FrontPageRenderTest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/frontpage/FrontPageScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/frontpage/FrontPageRenderTest.kt index 78b37c7c9c..d040320e22 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/frontpage/FrontPageScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/frontpage/FrontPageRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.courses.details.frontpage +package com.instructure.parentapp.ui.rendertests.courses.details.frontpage import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed @@ -31,7 +31,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class FrontPageScreenTest { +class FrontPageRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/summary/SummaryScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/summary/SummaryRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/summary/SummaryScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/summary/SummaryRenderTest.kt index 5715619c09..c6ef376040 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/summary/SummaryScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/summary/SummaryRenderTest.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.parentapp.ui.compose.courses.details.summary +package com.instructure.parentapp.ui.rendertests.courses.details.summary import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed @@ -35,7 +35,7 @@ import org.junit.runner.RunWith import java.util.Calendar @RunWith(AndroidJUnit4::class) -class SummaryScreenTest { +class SummaryRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/list/CoursesScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/list/CoursesRenderTest.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/list/CoursesScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/list/CoursesRenderTest.kt index e5724d8551..d207115a75 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/list/CoursesScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/list/CoursesRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.courses.list +package com.instructure.parentapp.ui.rendertests.courses.list import androidx.compose.ui.graphics.Color import androidx.compose.ui.test.assertHasClickAction @@ -35,7 +35,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class CoursesScreenTest { +class CoursesRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/login/createaccount/CreateAccountScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/login/createaccount/CreateAccountRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/login/createaccount/CreateAccountScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/login/createaccount/CreateAccountRenderTest.kt index d7a7747423..e094decf5b 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/login/createaccount/CreateAccountScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/login/createaccount/CreateAccountRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.login.createaccount +package com.instructure.parentapp.ui.rendertests.login.createaccount import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasText @@ -33,7 +33,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class CreateAccountScreenTest { +class CreateAccountRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/managestudents/ManageStudentsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/managestudents/ManageStudentsRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/managestudents/ManageStudentsScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/managestudents/ManageStudentsRenderTest.kt index d265788cf9..48ebfa8d83 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/managestudents/ManageStudentsScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/managestudents/ManageStudentsRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.managestudents +package com.instructure.parentapp.ui.rendertests.managestudents import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed @@ -39,7 +39,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class ManageStudentsScreenTest { +class ManageStudentsRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/notaparent/NotAParentScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/notaparent/NotAParentRenderTest.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/notaparent/NotAParentScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/notaparent/NotAParentRenderTest.kt index 99e9adc6be..f7013aca17 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/notaparent/NotAParentScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/notaparent/NotAParentRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.notaparent +package com.instructure.parentapp.ui.rendertests.notaparent import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed @@ -36,7 +36,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class NotAParentScreenTest { +class NotAParentRenderTest { @get:Rule val composeTestRule = createComposeRule() 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 d13e612fb6..e5340473d0 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 @@ -18,7 +18,7 @@ package com.instructure.parentapp.utils import androidx.compose.ui.test.junit4.createAndroidComposeRule -import com.instructure.canvas.espresso.common.pages.ReminderPage +import com.instructure.canvas.espresso.common.pages.AssignmentReminderPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventCreateEditPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage import com.instructure.canvas.espresso.common.pages.compose.CalendarFilterPage @@ -32,19 +32,19 @@ import com.instructure.canvas.espresso.common.pages.compose.InboxSignatureSettin import com.instructure.canvas.espresso.common.pages.compose.RecipientPickerPage import com.instructure.canvas.espresso.common.pages.compose.SettingsPage import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.AddStudentBottomPage -import com.instructure.parentapp.ui.pages.AlertsPage -import com.instructure.parentapp.ui.pages.AnnouncementDetailsPage -import com.instructure.parentapp.ui.pages.CourseDetailsPage -import com.instructure.parentapp.ui.pages.CoursesPage -import com.instructure.parentapp.ui.pages.CreateAccountPage -import com.instructure.parentapp.ui.pages.ManageStudentsPage -import com.instructure.parentapp.ui.pages.PairingCodePage -import com.instructure.parentapp.ui.pages.ParentInboxCoursePickerPage -import com.instructure.parentapp.ui.pages.QrPairingPage -import com.instructure.parentapp.ui.pages.StudentAlertSettingsPage -import com.instructure.parentapp.ui.pages.SummaryPage +import com.instructure.parentapp.ui.pages.compose.AddStudentBottomPage +import com.instructure.parentapp.ui.pages.compose.AlertsPage +import com.instructure.parentapp.ui.pages.compose.AnnouncementDetailsPage +import com.instructure.parentapp.ui.pages.compose.CourseDetailsPage +import com.instructure.parentapp.ui.pages.compose.CoursesPage +import com.instructure.parentapp.ui.pages.compose.CreateAccountPage +import com.instructure.parentapp.ui.pages.compose.ManageStudentsPage import com.instructure.parentapp.ui.pages.compose.NotAParentPage +import com.instructure.parentapp.ui.pages.compose.PairingCodePage +import com.instructure.parentapp.ui.pages.compose.ParentInboxCoursePickerPage +import com.instructure.parentapp.ui.pages.compose.QrPairingPage +import com.instructure.parentapp.ui.pages.compose.StudentAlertSettingsPage +import com.instructure.parentapp.ui.pages.compose.SummaryPage import org.junit.Rule @@ -78,7 +78,7 @@ abstract class ParentComposeTest : ParentTest() { protected val calendarToDoCreateUpdatePage = CalendarToDoCreateUpdatePage(composeTestRule) protected val calendarToDoDetailsPage = CalendarToDoDetailsPage(composeTestRule) protected val calendarFilterPage = CalendarFilterPage(composeTestRule) - protected val reminderPage = ReminderPage(composeTestRule) + protected val assignmentReminderPage = AssignmentReminderPage(composeTestRule) protected val inboxSignatureSettingsPage = InboxSignatureSettingsPage(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 40dbfce721..69fadbaef3 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 @@ -29,11 +29,11 @@ import com.instructure.canvas.espresso.common.pages.LoginSignInPage import com.instructure.espresso.ModuleItemInteractions import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage -import com.instructure.parentapp.ui.pages.FrontPagePage -import com.instructure.parentapp.ui.pages.HelpPage -import com.instructure.parentapp.ui.pages.LeftSideNavigationDrawerPage -import com.instructure.parentapp.ui.pages.SyllabusPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage +import com.instructure.parentapp.ui.pages.classic.FrontPagePage +import com.instructure.parentapp.ui.pages.classic.HelpPage +import com.instructure.parentapp.ui.pages.classic.LeftSideNavigationDrawerPage +import com.instructure.parentapp.ui.pages.compose.SyllabusPage abstract class ParentTest : CanvasTest() { diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/extensions/ParentTestExtensions.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/utils/extensions/ParentTestExtensions.kt index 6cb71e671b..281d6beec0 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/extensions/ParentTestExtensions.kt @@ -15,13 +15,14 @@ * */ -package com.instructure.parentapp.utils +package com.instructure.parentapp.utils.extensions import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.api.SeedApi import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.parentapp.features.login.LoginActivity +import com.instructure.parentapp.utils.ParentTest fun ParentTest.tokenLogin(user: CanvasUserApiModel) { diff --git a/apps/student/flank.yml b/apps/student/flank.yml index 45f1d8bccf..19b8cb5e65 100644 --- a/apps/student/flank.yml +++ b/apps/student/flank.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_coverage.yml b/apps/student/flank_coverage.yml index 3af12c3e0b..ee06d4aa34 100644 --- a/apps/student/flank_coverage.yml +++ b/apps/student/flank_coverage.yml @@ -19,7 +19,7 @@ gcloud: directories-to-pull: - /sdcard/ test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.OfflineE2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.OfflineE2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubCoverage device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_e2e.yml b/apps/student/flank_e2e.yml index 621608d4d8..063c351403 100644 --- a/apps/student/flank_e2e.yml +++ b/apps/student/flank_e2e.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_e2e_coverage.yml b/apps/student/flank_e2e_coverage.yml index 55a9ea0a1b..c7da259d52 100644 --- a/apps/student/flank_e2e_coverage.yml +++ b/apps/student/flank_e2e_coverage.yml @@ -19,8 +19,8 @@ gcloud: directories-to-pull: - /sdcard/ test-targets: - - annotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.OfflineE2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage + - annotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubCoverage device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_e2e_flaky.yml b/apps/student/flank_e2e_flaky.yml index 9a73a6fd64..f402c0b993 100644 --- a/apps/student/flank_e2e_flaky.yml +++ b/apps/student/flank_e2e_flaky.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.FlakyE2E + - annotation com.instructure.canvas.espresso.annotations.FlakyE2E device: - model: Nexus6P version: 26 diff --git a/apps/student/flank_e2e_knownbug.yml b/apps/student/flank_e2e_knownbug.yml index b1cb8ff730..5c88f700fd 100644 --- a/apps/student/flank_e2e_knownbug.yml +++ b/apps/student/flank_e2e_knownbug.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.KnownBug device: - model: Nexus6P version: 26 diff --git a/apps/student/flank_e2e_lowres.yml b/apps/student/flank_e2e_lowres.yml index d828548431..3ffe98d316 100644 --- a/apps/student/flank_e2e_lowres.yml +++ b/apps/student/flank_e2e_lowres.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: NexusLowRes version: 29 diff --git a/apps/student/flank_e2e_min.yml b/apps/student/flank_e2e_min.yml index 48817ea5d4..4f545a602f 100644 --- a/apps/student/flank_e2e_min.yml +++ b/apps/student/flank_e2e_min.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Nexus6P version: 26 diff --git a/apps/student/flank_e2e_offline.yml b/apps/student/flank_e2e_offline.yml index 0f091632dd..1fc4ef8a8c 100644 --- a/apps/student/flank_e2e_offline.yml +++ b/apps/student/flank_e2e_offline.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.OfflineE2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_landscape.yml b/apps/student/flank_landscape.yml index ebd8fc2896..4edd43e3fe 100644 --- a/apps/student/flank_landscape.yml +++ b/apps/student/flank_landscape.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubLandscape, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubLandscape, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_multi_api_level.yml b/apps/student/flank_multi_api_level.yml index b0967a38ec..46d04fd3f1 100644 --- a/apps/student/flank_multi_api_level.yml +++ b/apps/student/flank_multi_api_level.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubMultiAPILevel, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: NexusLowRes version: 27 diff --git a/apps/student/flank_tablet.yml b/apps/student/flank_tablet.yml index ba6afecd57..d53ad38661 100644 --- a/apps/student/flank_tablet.yml +++ b/apps/student/flank_tablet.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubTablet, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubTablet, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: MediumTablet.arm version: 29 diff --git a/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt b/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt new file mode 100644 index 0000000000..fc00cc637e --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.espresso + +import androidx.work.WorkerFactory +import com.instructure.student.util.BaseAppManager + +open class TestAppManager : BaseAppManager() { + + var workerFactory: WorkerFactory? = null + + override fun getWorkManagerFactory(): WorkerFactory { + return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginFindSchoolPageTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/LoginFindSchoolPageTest.kt deleted file mode 100644 index 3e59511e4a..0000000000 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginFindSchoolPageTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2018 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package com.instructure.student.ui - -import com.instructure.student.ui.utils.StudentTest -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test - -@HiltAndroidTest -class LoginFindSchoolPageTest: StudentTest() { - - // Runs live; no MockCanvas - @Test - override fun displaysPageObjects() { - loginLandingPage.clickFindMySchoolButton() - loginFindSchoolPage.assertPageObjects() - } -} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginLandingPageTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/LoginLandingPageTest.kt deleted file mode 100644 index c4e6f156b2..0000000000 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginLandingPageTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2018 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package com.instructure.student.ui - -import com.instructure.student.ui.utils.StudentTest -import com.instructure.espresso.filters.P1 -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test - -@HiltAndroidTest -class LoginLandingPageTest: StudentTest() { - - // Runs live; no MockCanvas - @Test - @P1 - override fun displaysPageObjects() { - loginLandingPage.assertPageObjects() - } - -} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginSignInPageTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/LoginSignInPageTest.kt deleted file mode 100644 index a09366509d..0000000000 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginSignInPageTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2018 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package com.instructure.student.ui - -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.enterDomain -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test - -@HiltAndroidTest -class LoginSignInPageTest: StudentTest() { - - // Runs live; no MockCanvas - @Test - override fun displaysPageObjects() { - loginLandingPage.clickFindMySchoolButton() - enterDomain() - loginFindSchoolPage.clickToolbarNextMenuItem() - loginSignInPage.assertPageObjects() - } -} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/AnnouncementsE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/AnnouncementsE2ETest.kt index ec0da2f0a3..eec078207e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/AnnouncementsE2ETest.kt @@ -14,20 +14,20 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.lang.Thread.sleep diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/CollaborationsE2ETest.kt similarity index 86% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/CollaborationsE2ETest.kt index c32d6c909e..bc426faa67 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/CollaborationsE2ETest.kt @@ -1,15 +1,15 @@ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.ui.pages.CollaborationsPage +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.student.ui.pages.classic.CollaborationsPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ConferencesE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ConferencesE2ETest.kt index ce6ea0b07e..0d4a3c27c0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ConferencesE2ETest.kt @@ -1,16 +1,16 @@ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.ConferencesApi import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DashboardE2ETest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DashboardE2ETest.kt index 875487918e..aab20d2faf 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DashboardE2ETest.kt @@ -14,20 +14,20 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DiscussionsE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DiscussionsE2ETest.kt index 9062629753..fac3cbf7e2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DiscussionsE2ETest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.os.SystemClock.sleep import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.DiscussionTopicsApi -import com.instructure.espresso.ViewUtils import com.instructure.espresso.getDateInCanvasFormat import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -107,7 +107,7 @@ class DiscussionsE2ETest: StudentTest() { discussionListPage.assertTopicDisplayed(announcement2.title) Log.d(STEP_TAG, "Navigate back to CourseBrowser Page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Navigate to Discussions Page.") courseBrowserPage.selectDiscussions() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt index 04fb9f8848..b0e5045ed2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.os.Environment import android.util.Log @@ -22,12 +22,13 @@ import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents import androidx.test.platform.app.InstrumentationRegistry -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.managers.DiscussionManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.weave.awaitApiResponse @@ -44,10 +45,9 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin -import com.instructure.student.ui.utils.uploadTextFile +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Rule import org.junit.Test @@ -153,7 +153,7 @@ class FilesE2ETest: StudentTest() { fileListPage.assertItemDisplayed(submissionUploadInfo.fileName) Log.d(STEP_TAG, "Navigate back to File List Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(ASSERTION_TAG, "Assert that there is a directory called 'unfiled' is displayed.") fileListPage.assertItemDisplayed("unfiled") // Our discussion attachment goes under "unfiled" @@ -165,7 +165,7 @@ class FilesE2ETest: StudentTest() { fileListPage.assertItemDisplayed(discussionAttachmentFile.name) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Select '${course.name}' course.") dashboardPage.selectCourse(course) @@ -190,7 +190,7 @@ class FilesE2ETest: StudentTest() { submissionDetailsPage.assertCommentAttachmentDisplayed(commentUploadInfo.fileName, student) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(4) + pressBackButton(4) Log.d(STEP_TAG, "Navigate to 'Files' menu in user left-side menu bar.") leftSideNavigationDrawerPage.clickFilesMenu() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt index ec06b1feea..aec21f7875 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt @@ -1,13 +1,13 @@ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers.withText -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.QuizzesApi @@ -22,8 +22,8 @@ import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.getDateInCanvasCalendarFormat import com.instructure.student.R import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/HelpMenuE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/HelpMenuE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/HelpMenuE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/HelpMenuE2ETest.kt index 42fa7d552b..4becc7b2a6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/HelpMenuE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/HelpMenuE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.intent.Intents -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText import com.instructure.student.R import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/LoginE2ETest.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/LoginE2ETest.kt index 1181b21559..3fae58f502 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/LoginE2ETest.kt @@ -14,16 +14,17 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.EnrollmentsApi @@ -36,8 +37,8 @@ import com.instructure.dataseeding.model.EnrollmentTypes.TEACHER_ENROLLMENT import com.instructure.dataseeding.util.CanvasNetworkAdapter import com.instructure.espresso.withIdlingResourceDisabled import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.extensions.enterDomain +import com.instructure.student.ui.utils.extensions.seedData import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -169,7 +170,7 @@ class LoginE2ETest : StudentTest() { validateUserAndRole(student, course, "Student") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Log out with '${student.name}' student.") leftSideNavigationDrawerPage.logout() @@ -181,7 +182,7 @@ class LoginE2ETest : StudentTest() { validateUserAndRole(teacher, course, "Teacher") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Log out with '${teacher.name}' teacher.") leftSideNavigationDrawerPage.logout() @@ -193,7 +194,7 @@ class LoginE2ETest : StudentTest() { validateUserAndRole(ta, course, "TA") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Log out with '${ta.name}' teacher assistant.") leftSideNavigationDrawerPage.logout() @@ -324,7 +325,7 @@ class LoginE2ETest : StudentTest() { validateUserAndRole(student, course,"Student" ) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Log out with '${student.name}' student.") leftSideNavigationDrawerPage.logout() @@ -343,6 +344,37 @@ class LoginE2ETest : StudentTest() { canvasNetworkSignInPage.assertPageObjects() } + @Test + fun testFindSchoolPageObjects() { + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(ASSERTION_TAG, "Assert that the Find School Page has been displayed.") + loginFindSchoolPage.assertPageObjects() + } + + @Test + fun testLoginLandingPageObjects() { + + Log.d(ASSERTION_TAG, "Assert that the Login Landing Page has been displayed.") + loginLandingPage.assertPageObjects() + } + + @Test + fun testLoginSignInPageObjects() { + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG, "Enter domain: 'mobileqa.beta.instructure.com', and click on the 'Next' button on the toolbar.") + enterDomain() + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(ASSERTION_TAG, "Assert that the Login SignIn Page has been displayed.") + loginSignInPage.assertPageObjects() + } + private fun loginWithUser(user: CanvasUserApiModel, lastSchoolSaved: Boolean = false) { Thread.sleep(5100) //Need to wait > 5 seconds before each login attempt because of new 'too many attempts' login policy on web. diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt index c03e3b3f0a..43e1ce6830 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt @@ -14,16 +14,16 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.dataseeding.api.ModulesApi @@ -36,8 +36,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/NotificationsE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/NotificationsE2ETest.kt index fcf9416b90..f7672ac3f7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/NotificationsE2ETest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.NoMatchingViewException -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.QuizzesApi @@ -35,8 +35,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.lang.Thread.sleep diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PagesE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PagesE2ETest.kt index 45a8f26960..eb1bd1d837 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PagesE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.PagesApi -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PushNotificationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PushNotificationsE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PushNotificationsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PushNotificationsE2ETest.kt index 860d644da9..7a903da517 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PushNotificationsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PushNotificationsE2ETest.kt @@ -1,11 +1,11 @@ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.student.BuildConfig import com.instructure.student.ui.utils.StudentComposeTest import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/QuizzesE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/QuizzesE2ETest.kt index 923201d888..14038602cd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/QuizzesE2ETest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.matcher.ViewMatchers.withId @@ -25,21 +25,21 @@ import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.DriverAtoms.getText import androidx.test.espresso.web.webdriver.DriverAtoms.webScrollIntoView import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.QuizzesApi import com.instructure.dataseeding.model.QuizAnswer import com.instructure.dataseeding.model.QuizQuestion import com.instructure.student.R -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers.containsString import org.junit.Test @@ -157,7 +157,7 @@ class QuizzesE2ETest: StudentTest() { .check(webMatches(getText(),containsString("LATEST"))) Log.d(STEP_TAG, "Navigate back to Course Browser Page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Navigate to Grades Page.") courseBrowserPage.selectGrades() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt index 3f03cf856d..0f78466c0b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.content.Intent import android.net.Uri @@ -24,8 +24,9 @@ import androidx.test.espresso.Espresso import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector -import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType @@ -33,9 +34,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Rule import org.junit.Test @@ -197,7 +197,7 @@ class ShareExtensionE2ETest: StudentTest() { Log.d(STEP_TAG, "Press back button to navigate back to the Dashboard Page.") assignmentDetailsPage.assertPageObjects() - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Assert that the Dashboard Page is displayed correctly.") dashboardPage.assertPageObjects() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/SyllabusE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/SyllabusE2ETest.kt index 8c97f277d1..39daa8c8be 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/SyllabusE2ETest.kt @@ -14,10 +14,10 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory @@ -29,8 +29,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/TodoE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/TodoE2ETest.kt index 8bac3991d3..896f7e2720 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/TodoE2ETest.kt @@ -1,11 +1,11 @@ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.refresh @@ -19,9 +19,9 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedAssignments -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedAssignments +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.lang.Thread.sleep diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/GradesElementaryE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/GradesElementaryE2ETest.kt index 431775a065..64ca48fb25 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/GradesElementaryE2ETest.kt @@ -14,16 +14,16 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.k5 +package com.instructure.student.ui.e2e.classic.k5 import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.GradingPeriodsApi import com.instructure.dataseeding.api.SubmissionsApi @@ -34,10 +34,10 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.page.getStringFromResource import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedDataForK5 -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.seedDataForK5 +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt index 5c7be151e0..e4b3e95757 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt @@ -14,30 +14,31 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.k5 +package com.instructure.student.ui.e2e.classic.k5 import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvasapi2.utils.toDate import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedDataForK5 -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.seedDataForK5 +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale @HiltAndroidTest class ImportantDatesE2ETest : StudentTest() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt index 04dc448f9a..18d84e3242 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt @@ -14,18 +14,17 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.k5 +package com.instructure.student.ui.e2e.classic.k5 import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvasapi2.type.AssetString +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType @@ -33,10 +32,10 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.espresso.page.getStringFromResource import com.instructure.espresso.page.withAncestor import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedDataForK5 -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.seedDataForK5 +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Rule import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/ManageOfflineContentE2ETest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/ManageOfflineContentE2ETest.kt index 55285c67ab..9e0ad68804 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/ManageOfflineContentE2ETest.kt @@ -14,22 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvasapi2.type.AssetString -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAllCoursesE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAllCoursesE2ETest.kt index 1529e89278..b4267ddba6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAllCoursesE2ETest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAnnouncementsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAnnouncementsE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAnnouncementsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAnnouncementsE2ETest.kt index f5efcd913e..fa6e4c2775 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAnnouncementsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAnnouncementsE2ETest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.DiscussionTopicsApi -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAssignmentsE2ETest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAssignmentsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAssignmentsE2ETest.kt index bc62e00f39..339e3aba5b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAssignmentsE2ETest.kt @@ -14,17 +14,18 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentGroupsApi import com.instructure.dataseeding.api.AssignmentsApi @@ -34,11 +35,10 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test @@ -235,7 +235,7 @@ class OfflineAssignmentsE2ETest : StudentComposeTest() { OfflineTestUtils.dismissNoInternetConnectionDialog() Log.d(STEP_TAG, "Navigate back to the Assignment List Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Click on the '${notSubmittedAssignment.name}' NOT submitted assignment.") assignmentListPage.clickAssignment(notSubmittedAssignment) @@ -254,7 +254,7 @@ class OfflineAssignmentsE2ETest : StudentComposeTest() { submissionDetailsPage.assertNoSubmissionEmptyView() Log.d(STEP_TAG, "Navigate back to the Assignment List Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Click on the '${gradedAssignment.name}' GRADED submitted assignment.") assignmentListPage.clickAssignment(gradedAssignment) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineConferencesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineConferencesE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineConferencesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineConferencesE2ETest.kt index f3dd1d7fbe..b9bc799389 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineConferencesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineConferencesE2ETest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.dataseeding.api.ConferencesApi -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineCourseBrowserE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineCourseBrowserE2ETest.kt index e17d4301c6..8ebc0ab19e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineCourseBrowserE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.pages.CourseBrowserPage +import com.instructure.canvas.espresso.annotations.OfflineE2E +import com.instructure.student.ui.pages.classic.CourseBrowserPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDashboardE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDashboardE2ETest.kt index 65093f02b9..b7f33e4fba 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDashboardE2ETest.kt @@ -14,23 +14,23 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDiscussionsE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDiscussionsE2ETest.kt index 0e204cb712..321952c93c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDiscussionsE2ETest.kt @@ -14,28 +14,28 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.espresso.getDateInCanvasFormat import com.instructure.student.R -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.openOverflowMenu -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.openOverflowMenu +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test @@ -89,7 +89,7 @@ class OfflineDiscussionsE2ETest : StudentTest() { discussionDetailsPage.assertEntryDisplayed("My reply") Log.d(STEP_TAG, "Navigate back to the Dashboard page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Open the '${course.name}' course's 'Manage Offline Content' page via the more menu of the Dashboard Page.") dashboardPage.clickCourseOverflowMenu(course.name, "Manage Offline Content") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineFilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineFilesE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineFilesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineFilesE2ETest.kt index dfd96a4b1c..5546afc96e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineFilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineFilesE2ETest.kt @@ -14,24 +14,24 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.dataseeding.api.FileFolderApi import com.instructure.dataseeding.model.FileUploadType -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin -import com.instructure.student.ui.utils.uploadTextFile +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.extensions.uploadTextFile +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineGradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineGradesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt index 925461cbda..a0aae2496b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineGradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt @@ -14,17 +14,17 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.QuizzesApi @@ -37,10 +37,10 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.getDateInCanvasCalendarFormat -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLeftSideMenuE2ETest.kt similarity index 89% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLeftSideMenuE2ETest.kt index c9285237ff..c48435baa6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLeftSideMenuE2ETest.kt @@ -14,23 +14,23 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertNoInternetConnectionDialog -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.dismissNoInternetConnectionDialog import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertNoInternetConnectionDialog +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.utils.offline.OfflineTestUtils.dismissNoInternetConnectionDialog import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLoginE2ETest.kt similarity index 93% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLoginE2ETest.kt index 753b40d2d8..9ac702bffd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLoginE2ETest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.model.CanvasUserApiModel -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertNoInternetConnectionDialog -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.dismissNoInternetConnectionDialog -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertNoInternetConnectionDialog +import com.instructure.student.ui.utils.offline.OfflineTestUtils.dismissNoInternetConnectionDialog +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineModulesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt index 1e7a7968db..49cd03c2a7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt @@ -14,16 +14,16 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.DiscussionTopicsApi @@ -36,12 +36,12 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePagesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePagesE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePagesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePagesE2ETest.kt index 2d3411748a..1d7dbfe403 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePagesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePagesE2ETest.kt @@ -14,27 +14,27 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.PagesApi -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePeopleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePeopleE2ETest.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePeopleE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePeopleE2ETest.kt index 519ca1e92c..6d6eb60128 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePeopleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePeopleE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.matcher.ViewMatchers import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSettingsE2ETest.kt similarity index 88% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSettingsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSettingsE2ETest.kt index 6955501151..2cab83b195 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSettingsE2ETest.kt @@ -14,23 +14,23 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertNoInternetConnectionDialog -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.dismissNoInternetConnectionDialog import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertNoInternetConnectionDialog +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.utils.offline.OfflineTestUtils.dismissNoInternetConnectionDialog import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyllabusE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyllabusE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyllabusE2ETest.kt index 8953e14e2c..8969573d0a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyllabusE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyllabusE2ETest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.SubmissionType @@ -30,10 +30,10 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.retryWithIncreasingDelay -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncProgressE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncProgressE2ETest.kt index 6dfa04fd8f..0c21032139 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncProgressE2ETest.kt @@ -14,23 +14,23 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.refresh -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncSettingsE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncSettingsE2ETest.kt index 9464b4ec0e..ccbf589970 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncSettingsE2ETest.kt @@ -14,20 +14,20 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.pandautils.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test @@ -107,7 +107,7 @@ class OfflineSyncSettingsE2ETest : StudentComposeTest() { offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly) Log.d(STEP_TAG, "Navigate back to Dashboard Page and logout.") - ViewUtils.pressBackButton(2) + pressBackButton(2) leftSideNavigationDrawerPage.logout() Log.d(STEP_TAG, "Click 'Find My School' button.") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/usergroups/UserGroupFilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/usergroups/UserGroupFilesE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/usergroups/UserGroupFilesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/usergroups/UserGroupFilesE2ETest.kt index 780bc91011..18cde2eb81 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/usergroups/UserGroupFilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/usergroups/UserGroupFilesE2ETest.kt @@ -14,12 +14,12 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.usergroups +package com.instructure.student.ui.e2e.classic.usergroups import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents -import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory @@ -27,8 +27,8 @@ import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.dataseeding.api.GroupsApi import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/AssignmentsE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/AssignmentsE2ETest.kt index 09079ceefb..bc5497b79b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/AssignmentsE2ETest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.os.SystemClock.sleep import android.util.Log @@ -22,15 +22,16 @@ import android.view.KeyEvent import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import androidx.test.rule.GrantPermissionRule -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.checkToastText import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentGroupsApi import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi @@ -47,16 +48,18 @@ import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.pandautils.utils.toFormattedString import com.instructure.student.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin -import com.instructure.student.ui.utils.uploadTextFile +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.FixMethodOrder import org.junit.Rule import org.junit.Test +import org.junit.runners.MethodSorters import java.util.Calendar @HiltAndroidTest +@FixMethodOrder(MethodSorters.NAME_ASCENDING) class AssignmentsE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit @@ -70,6 +73,117 @@ class AssignmentsE2ETest: StudentComposeTest() { android.Manifest.permission.CAMERA ) + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.E2E) + fun test01CommentsBelongToSubmissionAttempts() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(teachers = 1, courses = 1, students = 1) + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") + val pointsTextAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) + + Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Select course: '${course.name}'.") + dashboardPage.selectCourse(course) + + Log.d(STEP_TAG, "Navigate to course Assignments Page.") + courseBrowserPage.selectAssignments() + + Log.d(ASSERTION_TAG, "Assert that our assignments are present, along with any grade/date info.") + assignmentListPage.assertHasAssignment(pointsTextAssignment) + + Log.d(STEP_TAG, "Click on assignment '${pointsTextAssignment.name}'.") + assignmentListPage.clickAssignment(pointsTextAssignment) + + Log.d(ASSERTION_TAG, "Assert that the corresponding views are displayed on the Assignment Details Page, and there is no submission yet.") + assignmentDetailsPage.assertPageObjects() + assignmentDetailsPage.assertStatusNotSubmitted() + + Log.d(ASSERTION_TAG, "Assert that 'Submission & Rubric' label is displayed and navigate to Submission Details Page.") + assignmentDetailsPage.assertSubmissionAndRubricLabel() + assignmentDetailsPage.goToSubmissionDetails() + + Log.d(ASSERTION_TAG, "Assert that there is no submission yet for the '${pointsTextAssignment.name}' assignment.") + submissionDetailsPage.assertNoSubmissionEmptyView() + + Log.d(STEP_TAG, "Navigate back to Assignment Details page.") + Espresso.pressBack() + + Log.d(PREPARATION_TAG, "Submit assignment: '${pointsTextAssignment.name}' for student: '${student.name}'.") + SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY))) + + Log.d(ASSERTION_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.") + assignmentDetailsPage.refresh() + assignmentDetailsPage.assertStatusSubmitted() + assignmentDetailsPage.assertSubmissionAndRubricLabel() + + Log.d(PREPARATION_TAG, "Make another submission for assignment: '${pointsTextAssignment.name}' for student: '${student.name}'.") + val secondSubmissionAttempt = SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY))) + + Log.d(ASSERTION_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.") + assignmentDetailsPage.refresh() + assignmentDetailsPage.assertStatusSubmitted() + assignmentDetailsPage.assertSubmissionAndRubricLabel() + + Log.d(ASSERTION_TAG, "Assert that the spinner is displayed and the last/newest attempt is selected.") + assignmentDetailsPage.assertAttemptSpinnerDisplayed() + assignmentDetailsPage.assertAttemptInformation() + assignmentDetailsPage.assertSelectedAttempt(2) + + Log.d(STEP_TAG, "Navigate to submission details Comments Tab.") + assignmentDetailsPage.goToSubmissionDetails() + submissionDetailsPage.openComments() + + Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") + submissionDetailsPage.assertTextSubmissionDisplayedAsComment() + + val newComment = "Comment for second attempt" + Log.d(STEP_TAG, "Add a new comment ('$newComment') and send it.") + submissionDetailsPage.addAndSendComment(newComment) + handleWorkManagerTask("SubmissionWorker") + + Log.d(ASSERTION_TAG, "Assert that '$newComment' is displayed.") + submissionDetailsPage.assertCommentDisplayed(newComment, student) + + Log.d(STEP_TAG, "Select 'Attempt 1'.") + submissionDetailsPage.selectAttempt("Attempt 1") + + Log.d(ASSERTION_TAG, "Assert that the selected attempt is 'Attempt 1'.") + submissionDetailsPage.assertSelectedAttempt("Attempt 1") + + Log.d(STEP_TAG, "Open 'Comments' tab.") + submissionDetailsPage.openComments() + + Log.d(ASSERTION_TAG, "Assert that '$newComment' is NOT displayed because it belongs to 'Attempt 2'.") + submissionDetailsPage.assertCommentNotDisplayed(newComment, student) + + Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") + submissionDetailsPage.assertTextSubmissionDisplayedAsComment() + + Log.d(STEP_TAG, "Select 'Attempt 2'.") + submissionDetailsPage.selectAttempt("Attempt 2") + + Log.d(ASSERTION_TAG, "Assert that the selected attempt is 'Attempt 2'.") + submissionDetailsPage.assertSelectedAttempt("Attempt 2") + + Log.d(STEP_TAG, "Open 'Comments' tab.") + submissionDetailsPage.openComments() + + Log.d(ASSERTION_TAG, "Assert that '$newComment' is displayed because it belongs to 'Attempt 2'.") + submissionDetailsPage.assertCommentDisplayed(newComment, student) + + Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") + submissionDetailsPage.assertTextSubmissionDisplayedAsComment() + } + @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.E2E, SecondaryFeatureCategory.ASSIGNMENT_REMINDER) @@ -106,72 +220,72 @@ class AssignmentsE2ETest: StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert that the corresponding views are displayed on the Assignment Details Page." + "Assert that the reminder section is displayed as well.") assignmentDetailsPage.assertPageObjects() - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneHour = futureDueDate.apply { add(Calendar.HOUR, -1) } Log.d(STEP_TAG, "Select '1 Hour Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneHour) - reminderPage.selectTime(reminderDateOneHour) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneHour) + assignmentReminderPage.selectTime(reminderDateOneHour) Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Hour Before' again.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneHour) - reminderPage.selectTime(reminderDateOneHour) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneHour) + assignmentReminderPage.selectTime(reminderDateOneHour) Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice.") checkToastText(R.string.reminderAlreadySet, activityRule.activity) Log.d(STEP_TAG, "Remove the '1 Hour Before' reminder, confirm the deletion dialog.") - reminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) Log.d(ASSERTION_TAG, "Assert that the '1 Hour Before' reminder is not displayed any more.") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) futureDueDate.apply { add(Calendar.HOUR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneWeek = futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, -1) } Log.d(STEP_TAG, "Select '1 Week Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneWeek) - reminderPage.selectTime(reminderDateOneWeek) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneWeek) + assignmentReminderPage.selectTime(reminderDateOneWeek) Log.d(ASSERTION_TAG, "Assert that the '1 Week Before' reminder is not displayed, as it is in the past." + "Assert that a toast message is occurring which warns that we cannot pick up a reminder which has already passed (for example cannot pick '1 Week Before' reminder for an assignment which ends tomorrow).") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) checkToastText(R.string.reminderInPast, activityRule.activity) futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) } Log.d(STEP_TAG, "Select '1 Day Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneDay) - reminderPage.selectTime(reminderDateOneDay) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneDay) + assignmentReminderPage.selectTime(reminderDateOneDay) Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder and select '1 Day Before' again with custom date and time.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Day Before' again.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneDay) - reminderPage.selectTime(reminderDateOneDay) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneDay) + assignmentReminderPage.selectTime(reminderDateOneDay) Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice. (Because 1 days and 24 hours is the same)") checkToastText(R.string.reminderAlreadySet, activityRule.activity) @@ -179,22 +293,22 @@ class AssignmentsE2ETest: StudentComposeTest() { futureDueDate.apply { add(Calendar.DAY_OF_MONTH, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder and select the custom reminder option and select date and time.") - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(futureDate) - reminderPage.selectCustomTime(KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_0) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(futureDate) + assignmentReminderPage.selectCustomTime(KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_0) Log.d(ASSERTION_TAG, "Assert that the 'Invalid time' error is shown when we typed '0' hour and '0' minutes for the custom reminder.") - reminderPage.assertInvalidTimeShown() + assignmentReminderPage.assertInvalidTimeShown() Log.d(STEP_TAG, "Navigate back to Assignment List Page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Click on assignment '${alreadyPastAssignment.name}'.") assignmentListPage.clickAssignment(alreadyPastAssignment) Log.d(ASSERTION_TAG, "Assert that the reminder section is NOT displayed, because the '${alreadyPastAssignment.name}' assignment has already passed..") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @E2E @@ -230,64 +344,64 @@ class AssignmentsE2ETest: StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert that the corresponding views are displayed on the Assignment Details Page. Assert that the reminder section is displayed as well.") assignmentDetailsPage.assertPageObjects() - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneHour = futureDueDate.apply { add(Calendar.HOUR, -1) } Log.d(STEP_TAG, "Select '1 Hour Before'.") - reminderPage.clickBeforeReminderOption("1 Hour Before") + assignmentReminderPage.clickBeforeReminderOption("1 Hour Before") Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Hour Before' again.") - reminderPage.clickBeforeReminderOption("1 Hour Before") + assignmentReminderPage.clickBeforeReminderOption("1 Hour Before") Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice.") checkToastText(R.string.reminderAlreadySet, activityRule.activity) Log.d(STEP_TAG, "Remove the '1 Hour Before' reminder, confirm the deletion dialog.") - reminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) Log.d(ASSERTION_TAG, "Assert that the '1 Hour Before' reminder is not displayed any more.") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) futureDueDate.apply { add(Calendar.HOUR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneWeek = futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, -1) } Log.d(STEP_TAG, "Select '1 Week Before'.") - reminderPage.clickBeforeReminderOption("1 Week Before") + assignmentReminderPage.clickBeforeReminderOption("1 Week Before") Log.d(ASSERTION_TAG, "Assert that the '1 Week Before' reminder is not displayed, as it is in the past." + "Assert that a toast message is occurring which warns that we cannot pick up a reminder which has already passed (for example cannot pick '1 Week Before' reminder for an assignment which ends tomorrow).") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) composeTestRule.waitForIdle() checkToastText(R.string.reminderInPast, activityRule.activity) composeTestRule.waitForIdle() futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) } Log.d(STEP_TAG, "Select '1 Day Before'.") - reminderPage.clickBeforeReminderOption("1 Day Before") + assignmentReminderPage.clickBeforeReminderOption("1 Day Before") Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Day Before' again.") - reminderPage.clickBeforeReminderOption("1 Day Before") + assignmentReminderPage.clickBeforeReminderOption("1 Day Before") Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice. (Because 1 days and 24 hours is the same)") composeTestRule.waitForIdle() @@ -303,7 +417,7 @@ class AssignmentsE2ETest: StudentComposeTest() { assignmentListPage.clickAssignment(alreadyPastAssignment) Log.d(ASSERTION_TAG, "Assert that the reminder section is NOT displayed, because the '${alreadyPastAssignment.name}' assignment has already passed..") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @E2E @@ -807,121 +921,10 @@ class AssignmentsE2ETest: StudentComposeTest() { submissionDetailsPage.assertSelectedAttempt("Attempt 1") } - @E2E - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.E2E) - fun testCommentsBelongToSubmissionAttempts() { - - Log.d(PREPARATION_TAG, "Seeding data.") - val data = seedData(teachers = 1, courses = 1, students = 1) - val student = data.studentsList[0] - val teacher = data.teachersList[0] - val course = data.coursesList[0] - - Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") - val pointsTextAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) - - Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") - tokenLogin(student) - dashboardPage.waitForRender() - - Log.d(STEP_TAG, "Select course: '${course.name}'.") - dashboardPage.selectCourse(course) - - Log.d(STEP_TAG, "Navigate to course Assignments Page.") - courseBrowserPage.selectAssignments() - - Log.d(ASSERTION_TAG, "Assert that our assignments are present, along with any grade/date info.") - assignmentListPage.assertHasAssignment(pointsTextAssignment) - - Log.d(STEP_TAG, "Click on assignment '${pointsTextAssignment.name}'.") - assignmentListPage.clickAssignment(pointsTextAssignment) - - Log.d(ASSERTION_TAG, "Assert that the corresponding views are displayed on the Assignment Details Page, and there is no submission yet.") - assignmentDetailsPage.assertPageObjects() - assignmentDetailsPage.assertStatusNotSubmitted() - - Log.d(ASSERTION_TAG, "Assert that 'Submission & Rubric' label is displayed and navigate to Submission Details Page.") - assignmentDetailsPage.assertSubmissionAndRubricLabel() - assignmentDetailsPage.goToSubmissionDetails() - - Log.d(ASSERTION_TAG, "Assert that there is no submission yet for the '${pointsTextAssignment.name}' assignment.") - submissionDetailsPage.assertNoSubmissionEmptyView() - - Log.d(STEP_TAG, "Navigate back to Assignment Details page.") - Espresso.pressBack() - - Log.d(PREPARATION_TAG, "Submit assignment: '${pointsTextAssignment.name}' for student: '${student.name}'.") - SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY))) - - Log.d(ASSERTION_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.") - assignmentDetailsPage.refresh() - assignmentDetailsPage.assertStatusSubmitted() - assignmentDetailsPage.assertSubmissionAndRubricLabel() - - Log.d(PREPARATION_TAG, "Make another submission for assignment: '${pointsTextAssignment.name}' for student: '${student.name}'.") - val secondSubmissionAttempt = SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY))) - - Log.d(ASSERTION_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.") - assignmentDetailsPage.refresh() - assignmentDetailsPage.assertStatusSubmitted() - assignmentDetailsPage.assertSubmissionAndRubricLabel() - - Log.d(ASSERTION_TAG, "Assert that the spinner is displayed and the last/newest attempt is selected.") - assignmentDetailsPage.assertAttemptSpinnerDisplayed() - assignmentDetailsPage.assertAttemptInformation() - assignmentDetailsPage.assertSelectedAttempt(2) - - Log.d(STEP_TAG, "Navigate to submission details Comments Tab.") - assignmentDetailsPage.goToSubmissionDetails() - submissionDetailsPage.openComments() - - Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") - submissionDetailsPage.assertTextSubmissionDisplayedAsComment() - - val newComment = "Comment for second attempt" - Log.d(STEP_TAG, "Add a new comment ('$newComment') and send it.") - submissionDetailsPage.addAndSendComment(newComment) - handleWorkManagerTask("SubmissionWorker") - - Log.d(ASSERTION_TAG, "Assert that '$newComment' is displayed.") - submissionDetailsPage.assertCommentDisplayed(newComment, student) - - Log.d(STEP_TAG, "Select 'Attempt 1'.") - submissionDetailsPage.selectAttempt("Attempt 1") - - Log.d(ASSERTION_TAG, "Assert that the selected attempt is 'Attempt 1'.") - submissionDetailsPage.assertSelectedAttempt("Attempt 1") - - Log.d(STEP_TAG, "Open 'Comments' tab.") - submissionDetailsPage.openComments() - - Log.d(ASSERTION_TAG, "Assert that '$newComment' is NOT displayed because it belongs to 'Attempt 2'.") - submissionDetailsPage.assertCommentNotDisplayed(newComment, student) - - Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") - submissionDetailsPage.assertTextSubmissionDisplayedAsComment() - - Log.d(STEP_TAG, "Select 'Attempt 2'.") - submissionDetailsPage.selectAttempt("Attempt 2") - - Log.d(ASSERTION_TAG, "Assert that the selected attempt is 'Attempt 2'.") - submissionDetailsPage.assertSelectedAttempt("Attempt 2") - - Log.d(STEP_TAG, "Open 'Comments' tab.") - submissionDetailsPage.openComments() - - Log.d(ASSERTION_TAG, "Assert that '$newComment' is displayed because it belongs to 'Attempt 2'.") - submissionDetailsPage.assertCommentDisplayed(newComment, student) - - Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") - submissionDetailsPage.assertTextSubmissionDisplayedAsComment() - } - @E2E @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.E2E) - fun showOnlyLetterGradeOnDashboardAndAssignmentListPageE2E() { + fun testShowOnlyLetterGradeOnDashboardAndAssignmentListPageE2E() { Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(teachers = 1, courses = 1, students = 1) val student = data.studentsList[0] @@ -1114,7 +1117,7 @@ class AssignmentsE2ETest: StudentComposeTest() { Espresso.pressBack() Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(ASSERTION_TAG, "Assert that the course grade is F, as it is converted to letter grade because the disability of the restriction has not propagated yet.") dashboardPage.assertCourseGrade(course.name, "F") @@ -1127,7 +1130,7 @@ class AssignmentsE2ETest: StudentComposeTest() { @E2E @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.E2E) - fun showOnlyLetterGradeOnGradesPageE2E() { + fun testShowOnlyLetterGradeOnGradesPageE2E() { Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(teachers = 1, courses = 1, students = 1) val student = data.studentsList[0] diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt index 85c27a402b..cfdded6aaa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt @@ -14,16 +14,17 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi @@ -34,11 +35,10 @@ import com.instructure.dataseeding.model.UpdateCourse import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -77,7 +77,7 @@ class BookmarksE2ETest : StudentComposeTest() { assignmentDetailsPage.addBookmark(bookmarkName) Log.d(STEP_TAG, "Navigate back to Bookmarks page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Click on the 'Bookmarks' menu within the left side menu to open the Bookmarks page.") leftSideNavigationDrawerPage.clickBookmarksMenu() @@ -160,7 +160,7 @@ class BookmarksE2ETest : StudentComposeTest() { assignmentDetailsPage.addBookmark(bookmarkName) Log.d(STEP_TAG, "Navigate back to the Dashboard page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Click on the 'Bookmarks' menu within the left side menu to open the Bookmarks page.") leftSideNavigationDrawerPage.clickBookmarksMenu() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt index e403b0d867..661fd12c56 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt @@ -16,16 +16,16 @@ package com.instructure.student.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.espresso.getDateInCanvasCalendarFormat import com.instructure.pandautils.features.calendar.CalendarPrefs import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CourseBrowserE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CourseBrowserE2ETest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CourseBrowserE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CourseBrowserE2ETest.kt index 4e2544ae53..56c7d47000 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CourseBrowserE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CourseBrowserE2ETest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.util.Log import androidx.compose.ui.test.assertIsDisplayed @@ -24,12 +24,12 @@ import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.requestFocus import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvasapi2.models.SmartSearchContentType import com.instructure.canvasapi2.models.SmartSearchFilter import com.instructure.dataseeding.api.AssignmentsApi @@ -44,8 +44,8 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/InboxE2ETest.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/InboxE2ETest.kt index cb5a5833ce..fe3596f126 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/InboxE2ETest.kt @@ -14,25 +14,25 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.os.SystemClock.sleep import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.ReleaseExclude import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.ReleaseExclude import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/PeopleE2ETest.kt similarity index 93% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/PeopleE2ETest.kt index 639524adf7..a08306675b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/PeopleE2ETest.kt @@ -14,18 +14,18 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -95,7 +95,7 @@ class PeopleE2ETest : StudentComposeTest() { composeTestRule.waitForIdle() Log.d(STEP_TAG, "Navigate back to the Dashboard (Course List) Page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) composeTestRule.waitForIdle() Log.d(STEP_TAG, "Sign out with '${student1.name}' student.") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/SettingsE2ETest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/SettingsE2ETest.kt index 404cce6edf..7a62e86529 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/SettingsE2ETest.kt @@ -14,34 +14,34 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.content.Intent import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory +import com.instructure.canvas.espresso.IntentActionMatcher import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.utils.RemoteConfigParam import com.instructure.canvasapi2.utils.RemoteConfigUtils import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.dataseeding.util.CanvasNetworkAdapter -import com.instructure.espresso.ViewUtils import com.instructure.pandautils.utils.AppTheme import com.instructure.student.BuildConfig import com.instructure.student.R -import com.instructure.student.ui.utils.IntentActionMatcher import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Assert @@ -89,7 +89,7 @@ class SettingsE2ETest : StudentComposeTest() { profileSettingsPage.changeUserNameTo(newUserName) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(ASSERTION_TAG, "Assert that the Dashboard Page has been displayed correctly.") leftSideNavigationDrawerPage.assertUserLoggedIn(newUserName) @@ -428,7 +428,7 @@ class SettingsE2ETest : StudentComposeTest() { CoursesApi.concludeCourse(course.id) // Need to conclude the course because otherwise there would be too much course with time on the dedicated user's dashboard. Log.d(STEP_TAG, "Navigate back to Dashboard.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Open the Left Side Menu.") dashboardPage.openLeftSideMenu() @@ -545,7 +545,7 @@ class SettingsE2ETest : StudentComposeTest() { inboxSignatureSettingsPage.assertSignatureEnabledState(true) Log.d(STEP_TAG, "Navigate back to the Dashboard.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open Inbox Page.") dashboardPage.clickInboxTab() @@ -652,7 +652,7 @@ class SettingsE2ETest : StudentComposeTest() { inboxComposePage.assertBodyText("") Log.d(STEP_TAG, "Navigate back to the Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Left Side Navigation Drawer menu.") dashboardPage.openLeftSideMenu() @@ -695,7 +695,7 @@ class SettingsE2ETest : StudentComposeTest() { inboxComposePage.assertBodyText("\n\n---\nLoyal member of Instructure") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Click on 'Change User' button on the Left Side Navigation Drawer menu.") leftSideNavigationDrawerPage.clickChangeUserMenu() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/HomeroomE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/HomeroomE2ETest.kt index 42d3fae041..b729230ee2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/HomeroomE2ETest.kt @@ -14,17 +14,17 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.k5 +package com.instructure.student.ui.e2e.compose.k5 import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType @@ -33,10 +33,10 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.page.getStringFromResource import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedDataForK5 -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.seedDataForK5 +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import org.threeten.bp.OffsetDateTime diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ResourcesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/ResourcesE2ETest.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ResourcesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/ResourcesE2ETest.kt index 6f94255daf..4425e1180f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ResourcesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/ResourcesE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.k5 +package com.instructure.student.ui.e2e.compose.k5 import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.model.CanvasUserApiModel -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedDataForK5 -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.seedDataForK5 +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt index 305e443bdc..3f02c95c28 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt @@ -19,23 +19,23 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addGroupToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addGroupToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.models.User -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt index 3eee878026..44c8737c32 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -25,12 +25,12 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.checkToastText -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -40,8 +40,8 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.pandautils.utils.toFormattedString import com.instructure.student.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.routeTo -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.routeTo +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -400,7 +400,7 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentListPage.clickAssignment(assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @Test @@ -413,7 +413,7 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentListPage.clickAssignment(assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @Test @@ -428,7 +428,7 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentListPage.clickAssignment(assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @Test @@ -445,12 +445,12 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { goToAssignmentList() assignmentListPage.clickAssignment(assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) } @Test @@ -467,17 +467,17 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { goToAssignmentList() assignmentListPage.clickAssignment(assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) - reminderPage.removeReminderWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderCalendar.time.toFormattedString()) - reminderPage.assertReminderNotDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderCalendar.time.toFormattedString()) } @Test @@ -494,10 +494,10 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { goToAssignmentList() assignmentListPage.clickAssignment(assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) checkToastText(R.string.reminderInPast, activityRule.activity) } @@ -517,15 +517,15 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentListPage.clickAssignment(assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) checkToastText(R.string.reminderAlreadySet, activityRule.activity) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt index 6ab549a22a..fe5fb60df1 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt @@ -19,17 +19,17 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CourseSettings import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt index 4de9bd32f9..0d050ae7b9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt @@ -24,17 +24,17 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addBookmark -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addBookmark +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvas.espresso.refresh import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt index e7ec64c5ad..cd57303fc2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt @@ -21,16 +21,16 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Grades import com.instructure.canvasapi2.models.Tab import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt index 929cfd601b..1f515925b2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt @@ -27,15 +27,15 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addPageToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addPageToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Tab import com.instructure.student.R import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.CoreMatchers.containsString import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt index baf444bf42..cc94803123 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt @@ -22,9 +22,9 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAccountNotification -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAccountNotification +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Grades @@ -32,7 +32,7 @@ import com.instructure.pandautils.di.NetworkStateProviderModule import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.student.espresso.fakes.FakeNetworkStateProvider import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt index 9a807b921f..8c498c7184 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt @@ -21,25 +21,25 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addReplyToDiscussion -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addReplyToDiscussion +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.DiscussionEntry import com.instructure.canvasapi2.models.RemoteFile import com.instructure.canvasapi2.models.Tab -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Assert.assertNotNull import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt index c77a537379..e7cec296db 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt @@ -20,11 +20,11 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt index bad0d87003..7a9703597d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt @@ -21,15 +21,15 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCourseWithEnrollment +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Enrollment import com.instructure.espresso.page.getStringFromResource import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt index 16579a02bd..4f5545bb7f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt @@ -23,22 +23,22 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addFileToFolder -import com.instructure.canvas.espresso.mockCanvas.addFolderToCourse -import com.instructure.canvas.espresso.mockCanvas.addGroupToCourse -import com.instructure.canvas.espresso.mockCanvas.addPageToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addFileToFolder +import com.instructure.canvas.espresso.mockcanvas.addFolderToCourse +import com.instructure.canvas.espresso.mockcanvas.addGroupToCourse +import com.instructure.canvas.espresso.mockcanvas.addPageToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvas.espresso.refresh import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.Page import com.instructure.canvasapi2.models.Tab -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt index 3566665576..0b90b7393d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt @@ -20,21 +20,21 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseWithEnrollment +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Enrollment import com.instructure.espresso.page.getStringFromResource import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt index 77afadd9d0..70a6f702dd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt @@ -18,15 +18,15 @@ package com.instructure.student.ui.interaction import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.StubTablet import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addAssignmentCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.StubTablet +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addAssignmentCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -34,9 +34,9 @@ import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt index 489c813e44..d2a00b7d67 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt @@ -23,17 +23,15 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.StubTablet -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.di.UpdateModule import com.instructure.pandautils.update.UpdateManager import com.instructure.pandautils.update.UpdatePrefs import com.instructure.student.R import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt index 9310441190..fd27493201 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt @@ -27,18 +27,18 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addItemToModule -import com.instructure.canvas.espresso.mockCanvas.addLTITool -import com.instructure.canvas.espresso.mockCanvas.addModuleToCourse -import com.instructure.canvas.espresso.mockCanvas.addPageToCourse -import com.instructure.canvas.espresso.mockCanvas.addQuestionToQuiz -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addItemToModule +import com.instructure.canvas.espresso.mockcanvas.addLTITool +import com.instructure.canvas.espresso.mockcanvas.addModuleToCourse +import com.instructure.canvas.espresso.mockcanvas.addPageToCourse +import com.instructure.canvas.espresso.mockcanvas.addQuestionToQuiz +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -56,9 +56,9 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.R -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt index 9b370a82ed..11e6687383 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt @@ -26,8 +26,8 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.ApiPrefs @@ -39,8 +39,8 @@ import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.student.R import com.instructure.student.espresso.fakes.FakeNetworkStateProvider import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt index 71c6db7740..6ec94e8480 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt @@ -19,12 +19,12 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionStreamItem -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionStreamItem +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -33,7 +33,7 @@ import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt index cd06987a98..3e5804dcc1 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt @@ -28,17 +28,16 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Tab import com.instructure.dataseeding.util.Randomizer import com.instructure.pandautils.R import com.instructure.pandautils.utils.StorageUtils import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt index 6c8da16aa8..75c0eeb1b5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt @@ -30,14 +30,14 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAnnotation -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAnnotation +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -47,8 +47,8 @@ import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader import com.instructure.pandautils.utils.filecache.FileCache import com.instructure.student.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.routeTo -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.routeTo +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt index d3339ff86d..ae12cfd141 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt @@ -19,12 +19,12 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.routeTo -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.routeTo +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt index 05b8c5b8c0..419e0b450a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt @@ -37,20 +37,20 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment import com.instructure.pandautils.utils.FilePrefs import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt index b2e05bcc1a..c459c4cffa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt @@ -10,12 +10,12 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addUserPermissions -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addUserPermissions +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.student.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Assert.assertTrue diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt index cd90ead0f5..3ca5edd7b0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt @@ -19,13 +19,13 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Quiz import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt index 1f4ed289cb..a31a7fe86a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt @@ -20,15 +20,15 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment -import com.instructure.canvas.espresso.mockCanvas.addEnrollment -import com.instructure.canvas.espresso.mockCanvas.addLTITool -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCourseWithEnrollment +import com.instructure.canvas.espresso.mockcanvas.addEnrollment +import com.instructure.canvas.espresso.mockcanvas.addLTITool +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Enrollment -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt index da968141ff..2f8d1f77f4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt @@ -18,15 +18,15 @@ package com.instructure.student.ui.interaction import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.StubLandscape -import com.instructure.canvas.espresso.StubTablet import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addTodo -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.StubLandscape +import com.instructure.canvas.espresso.annotations.StubTablet +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addTodo +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -34,10 +34,10 @@ import com.instructure.canvasapi2.utils.toApiString import com.instructure.espresso.page.getStringFromResource import com.instructure.pandautils.utils.date.DateTimeProvider import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.di.FakeDateTimeProvider -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt index 6a127b441c..daee0a97c7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt @@ -18,15 +18,15 @@ package com.instructure.student.ui.interaction import android.app.Activity import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.StubMultiAPILevel import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.StubMultiAPILevel +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt index 09d766e2be..b8e1af92f1 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt @@ -23,16 +23,16 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.StubCoverage -import com.instructure.canvas.espresso.StubTablet -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.annotations.StubCoverage +import com.instructure.canvas.espresso.annotations.StubTablet +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.User import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.io.File diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt index a99ffde9bc..c95950398d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt @@ -17,9 +17,9 @@ package com.instructure.student.ui.interaction import com.instructure.canvas.espresso.common.interaction.CalendarInteractionTest import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.User @@ -27,10 +27,10 @@ import com.instructure.espresso.ModuleItemInteractions import com.instructure.student.BuildConfig import com.instructure.student.R import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage -import com.instructure.student.ui.pages.DiscussionDetailsPage +import com.instructure.student.ui.pages.classic.DashboardPage +import com.instructure.student.ui.pages.classic.DiscussionDetailsPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateEventInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateEventInteractionTest.kt index 31ae49008e..d8f4b8d97c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateEventInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateEventInteractionTest.kt @@ -16,14 +16,14 @@ package com.instructure.student.ui.interaction 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.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateToDoInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateToDoInteractionTest.kt index 93d37f50ac..3f30c41fa3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateToDoInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateToDoInteractionTest.kt @@ -16,14 +16,14 @@ package com.instructure.student.ui.interaction 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.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentEventDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentEventDetailsInteractionTest.kt index 41e571f398..f13bc9fdd1 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentEventDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentEventDetailsInteractionTest.kt @@ -16,13 +16,13 @@ package com.instructure.student.ui.interaction 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.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt index 45030590eb..2fa1ef1776 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt @@ -22,19 +22,19 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.InboxComposeInteractionTest import com.instructure.canvas.espresso.common.pages.InboxPage -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.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +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.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -51,9 +51,9 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.User import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt index 33f3ca7445..ce509396f5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt @@ -16,17 +16,17 @@ package com.instructure.student.ui.interaction 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.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.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt index 5e1480cb59..e17a283289 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt @@ -20,17 +20,17 @@ 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.InboxSignatureInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -43,9 +43,9 @@ import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.managers.graphql.SubmissionGradeManager import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.LeftSideNavigationDrawerPage +import com.instructure.student.ui.pages.classic.LeftSideNavigationDrawerPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentSmartSearchInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentSmartSearchInteractionTest.kt index 901e4580d6..ffc1da8c8f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentSmartSearchInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentSmartSearchInteractionTest.kt @@ -21,9 +21,9 @@ import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.requestFocus import com.instructure.canvas.espresso.common.interaction.SmartSearchInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.DiscussionTopicHeader @@ -32,11 +32,11 @@ import com.instructure.canvasapi2.models.Tab import com.instructure.espresso.ModuleItemInteractions import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage -import com.instructure.student.ui.pages.DiscussionDetailsPage -import com.instructure.student.ui.pages.PageDetailsPage +import com.instructure.student.ui.pages.classic.DashboardPage +import com.instructure.student.ui.pages.classic.DiscussionDetailsPage +import com.instructure.student.ui.pages.classic.PageDetailsPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentToDoDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentToDoDetailsInteractionTest.kt index d666ea9436..85ff46ecdd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentToDoDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentToDoDetailsInteractionTest.kt @@ -17,15 +17,15 @@ package com.instructure.student.ui.interaction import android.app.Activity 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.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.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt index 06ec68e8ea..c20f50172a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt @@ -24,17 +24,17 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addRubricToAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addRubricToAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -45,9 +45,9 @@ import com.instructure.canvasapi2.models.RubricCriterion import com.instructure.canvasapi2.models.RubricCriterionRating import com.instructure.canvasapi2.models.SubmissionComment import com.instructure.espresso.handleWorkManagerTask -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -99,7 +99,6 @@ class SubmissionDetailsInteractionTest : StudentTest() { submissionDetailsPage.assertCommentDisplayed("Hey!", data.users.values.first()) } - @Stub @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) fun testComments_addCommentToMultipleAttemptSubmission() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt index 6d1b6792db..8f874664db 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt @@ -19,12 +19,12 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.addCourseSettings -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.addCourseSettings +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -35,7 +35,7 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt index 2235fcab13..08975f7866 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt @@ -17,16 +17,15 @@ package com.instructure.student.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.pandautils.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt index d7cf229736..2e6b05b79d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt @@ -22,15 +22,15 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.StubLandscape -import com.instructure.canvas.espresso.StubMultiAPILevel import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.StubLandscape +import com.instructure.canvas.espresso.annotations.StubMultiAPILevel +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -40,7 +40,7 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt index 56d7a4ec4e..aba68bc973 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt @@ -30,15 +30,15 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.StubCoverage import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.annotations.StubCoverage +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.pandautils.utils.Const import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.core.AllOf.allOf import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AllCoursesPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AllCoursesPage.kt index 193f5cfd8d..25071d31dc 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AllCoursesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.appcompat.widget.AppCompatImageView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnotationCommentListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnotationCommentListPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnotationCommentListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnotationCommentListPage.kt index af8e4ec2e3..7ea7db0296 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnotationCommentListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnotationCommentListPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers import com.instructure.canvas.espresso.scrollRecyclerView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnouncementListPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnouncementListPage.kt index 26c6b09593..43b34e1f4e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnouncementListPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import com.instructure.espresso.Searchable diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/BookmarkPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/BookmarkPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/BookmarkPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/BookmarkPage.kt index 8fa44a4c12..fde56600b7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/BookmarkPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/BookmarkPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.appcompat.widget.AppCompatButton import androidx.test.espresso.Espresso diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CanvasWebViewPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CanvasWebViewPage.kt index cc2a97e5eb..f02cd80651 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CanvasWebViewPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.annotation.StringRes import androidx.test.espresso.assertion.ViewAssertions.doesNotExist diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CollaborationsPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CollaborationsPage.kt index 4d40a43b7d..f5b1f8beea 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CollaborationsPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceDetailsPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceDetailsPage.kt index e18d491a3c..0a68fd921a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.hasSibling import com.instructure.espresso.assertDisplayed diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceListPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceListPage.kt index 2d7c19a8f8..1384001a0e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers.hasSibling diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferencesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferencesPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferencesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferencesPage.kt index 8053db226c..f105b7fcbe 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferencesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferencesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches @@ -111,7 +111,7 @@ object ConferencesPage { /** Assert that a conference with the specified name/title is displayed on the screen. */ fun assertConferenceTitlePresent(title: String) { Web.onWebView(Matchers.allOf(ViewMatchers.withId(R.id.contentWebView), ViewMatchers.isDisplayed())) - .withElement(transform(findConferenceTitleAtom(title), {evaluation -> + .withElement(transform(findConferenceTitleAtom(title), { evaluation -> evaluation.value as ElementReference})) .perform(webScrollIntoView()) .check(webMatches(getText(), containsString(title))) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseBrowserPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseBrowserPage.kt index 9990eea67a..a5a9fbf537 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseBrowserPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import android.widget.LinearLayout diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseGradesPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseGradesPage.kt index dd17e17206..d3188db641 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseGradesPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.os.SystemClock.sleep import android.view.View diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DashboardPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DashboardPage.kt index a05b8431d4..360ad84450 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DashboardPage.kt @@ -16,7 +16,7 @@ */ @file:Suppress("unused") -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.appcompat.widget.SwitchCompat @@ -38,6 +38,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import com.instructure.canvas.espresso.scrollRecyclerView +import com.instructure.canvas.espresso.waitForViewToDisappear import com.instructure.canvas.espresso.withCustomConstraints import com.instructure.canvasapi2.models.AccountNotification import com.instructure.canvasapi2.models.Course @@ -73,7 +74,6 @@ import com.instructure.espresso.scrollTo import com.instructure.espresso.swipeDown import com.instructure.espresso.waitForCheck import com.instructure.student.R -import com.instructure.student.ui.utils.ViewUtils import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.containsString @@ -440,7 +440,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { //OfflineMethod fun waitForSyncProgressDownloadStartedNotificationToDisappear() { - ViewUtils.waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_downloadStarting), 30) + waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_downloadStarting), 30) } //OfflineMethod @@ -455,7 +455,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { //OfflineMethod fun waitForSyncProgressStartingNotificationToDisappear() { - ViewUtils.waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_syncingOfflineContent), 30) + waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_syncingOfflineContent), 30) } //OfflineMethod diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionDetailsPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionDetailsPage.kt index db61d656e4..8d25de1dd7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.web.assertion.WebViewAssertions.webContent diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt index 6d66e65872..7c9e3433e8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.hasSibling diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileChooserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileChooserPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileChooserPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileChooserPage.kt index 6ae46f177b..5fce43521d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileChooserPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileChooserPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.doesNotExist diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileListPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileListPage.kt index cec5e6fb4d..ccd1da8500 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.appcompat.widget.AppCompatButton import androidx.test.espresso.Espresso diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GoToQuizPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GoToQuizPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/GoToQuizPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GoToQuizPage.kt index 9d6cc3af2c..d60f3d9696 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GoToQuizPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GoToQuizPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import com.instructure.espresso.ModuleItemInteractions import com.instructure.espresso.OnViewWithText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GradesPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GradesPage.kt index 1bb0b641b4..3c59c3d673 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GradesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.contrib.RecyclerViewActions diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GroupBrowserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GroupBrowserPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/GroupBrowserPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GroupBrowserPage.kt index 3ea8d62fc5..9792cd6bd8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GroupBrowserPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GroupBrowserPage.kt @@ -1,4 +1,4 @@ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/HelpPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/HelpPage.kt index 60b06ee29d..edef884447 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/HelpPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.app.Instrumentation import androidx.test.espresso.Espresso diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/LeftSideNavigationDrawerPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/LeftSideNavigationDrawerPage.kt index 6b5bed87d4..0f94aa3466 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/LeftSideNavigationDrawerPage.kt @@ -1,4 +1,4 @@ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.appcompat.widget.SwitchCompat diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModuleProgressionPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModuleProgressionPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModuleProgressionPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModuleProgressionPage.kt index c6e6aaca19..5d7d79c96c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModuleProgressionPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModuleProgressionPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.Espresso.onView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModulesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModulesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt index 5578e2e440..36d7a8fe5d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModulesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.PerformException @@ -26,6 +26,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withText +import com.instructure.canvas.espresso.ImageViewDrawableMatcher import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvas.espresso.withCustomConstraints import com.instructure.canvasapi2.models.Assignment @@ -48,7 +49,6 @@ import com.instructure.espresso.scrollTo import com.instructure.espresso.waitForCheck import com.instructure.pandautils.utils.color import com.instructure.student.R -import com.instructure.student.ui.utils.ImageViewDrawableMatcher import org.hamcrest.Matchers.allOf class ModulesPage : BasePage(R.id.modulesPage) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NotificationPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/NotificationPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/NotificationPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/NotificationPage.kt index 97747458eb..7d000597cd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NotificationPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/NotificationPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso.onView import androidx.test.espresso.NoMatchingViewException diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageDetailsPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageDetailsPage.kt index 2aca1f131e..f7e9d5e01f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageListPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageListPage.kt index 430678fc1f..0424bc4619 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PairObserverPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PairObserverPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PairObserverPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PairObserverPage.kt index d8a613c064..d692cd4016 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PairObserverPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PairObserverPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PandaAvatarPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PandaAvatarPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PandaAvatarPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PandaAvatarPage.kt index 5bb261d7ef..235df4a65d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PandaAvatarPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PandaAvatarPage.kt @@ -1,4 +1,4 @@ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PeopleListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PeopleListPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PeopleListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PeopleListPage.kt index 6254ddc991..f8814d9c37 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PeopleListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PeopleListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.recyclerview.widget.RecyclerView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PersonDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PersonDetailsPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PersonDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PersonDetailsPage.kt index c955c7fd1a..de48f1026e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PersonDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PersonDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PickerSubmissionUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PickerSubmissionUploadPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt index a37fd598af..97d3136adf 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PickerSubmissionUploadPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import com.instructure.espresso.OnViewWithId diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ProfileSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ProfileSettingsPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ProfileSettingsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ProfileSettingsPage.kt index fa6d671d36..750c32bd45 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ProfileSettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ProfileSettingsPage.kt @@ -1,4 +1,4 @@ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PushNotificationsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PushNotificationsPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PushNotificationsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PushNotificationsPage.kt index b4d1888d2e..fa464477dd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PushNotificationsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PushNotificationsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QRLoginPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QRLoginPage.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/QRLoginPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QRLoginPage.kt index e59ec44e5f..1fbb8871e4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QRLoginPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QRLoginPage.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * - */ package com.instructure.student.ui.pages + */ package com.instructure.student.ui.pages.classic import com.instructure.espresso.OnViewWithContentDescription import com.instructure.espresso.OnViewWithId diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizListPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizListPage.kt index 8a88074602..bbe2201c8b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.test.espresso.action.ViewActions.swipeDown diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizTakingPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizTakingPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizTakingPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizTakingPage.kt index 4e9a92f0da..af523304f5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizTakingPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizTakingPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.web.webdriver.Locator diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/RemoteConfigSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/RemoteConfigSettingsPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/RemoteConfigSettingsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/RemoteConfigSettingsPage.kt index 5edda4a5ef..73ea2f4b18 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/RemoteConfigSettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/RemoteConfigSettingsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import android.widget.EditText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionStatusPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionStatusPage.kt index 83d8ba9a25..35c3898cc7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionStatusPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.assertHasText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionTargetPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionTargetPage.kt index 0d0be79fae..259f18a3bb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionTargetPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso.onData import androidx.test.espresso.assertion.ViewAssertions diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt index 48d5c0c588..8622e5c1f5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.appcompat.widget.AppCompatButton import androidx.test.espresso.Espresso diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsEmptyContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsEmptyContentPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsEmptyContentPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsEmptyContentPage.kt index 3500562c7c..3bb64d00f3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsEmptyContentPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsEmptyContentPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import com.instructure.espresso.page.BasePage import com.instructure.student.R diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsPage.kt index d969b499f7..48c7f51907 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.action.ViewActions.click @@ -52,7 +52,7 @@ import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.espresso.replaceText import com.instructure.student.R -import com.instructure.student.ui.pages.renderPages.SubmissionCommentsRenderPage +import com.instructure.student.ui.rendertests.renderpages.SubmissionCommentsRenderPage import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.anyOf import org.hamcrest.Matchers.containsString diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SyllabusPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SyllabusPage.kt index 00114048ff..7fc11742d9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SyllabusPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TextSubmissionUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TextSubmissionUploadPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/TextSubmissionUploadPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TextSubmissionUploadPage.kt index bb5e6e1d91..c5095fca40 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TextSubmissionUploadPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TextSubmissionUploadPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView import com.instructure.canvas.espresso.explicitClick diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TodoPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TodoPage.kt index 2ae4b22f62..fde50e7e21 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TodoPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers.withText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/UrlSubmissionUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/UrlSubmissionUploadPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/UrlSubmissionUploadPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/UrlSubmissionUploadPage.kt index e5eddf16b5..6d5cdb42e3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/UrlSubmissionUploadPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/UrlSubmissionUploadPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.test.espresso.UiController diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryCoursePage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryCoursePage.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryCoursePage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryCoursePage.kt index 5254ab75c0..38de21de61 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryCoursePage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryCoursePage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import com.instructure.espresso.assertDisplayed import com.instructure.espresso.matchers.WaitForViewMatcher diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryDashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryDashboardPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryDashboardPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryDashboardPage.kt index e70ea4f77e..94c79b7972 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryDashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryDashboardPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/HomeroomPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/HomeroomPage.kt index 86c3a2a0db..7df4081f6e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/HomeroomPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import androidx.test.espresso.PerformException import androidx.test.espresso.assertion.ViewAssertions diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ImportantDatesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ImportantDatesPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ImportantDatesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ImportantDatesPage.kt index 8958de26f9..6a7e466a4c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ImportantDatesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ImportantDatesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import android.view.View import androidx.test.espresso.NoMatchingViewException diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ResourcesPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ResourcesPage.kt index bd6caa932d..f2e4978081 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ResourcesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/SchedulePage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/SchedulePage.kt index 0d6439244f..c46a47aaed 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/SchedulePage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import android.view.View import androidx.test.espresso.NoMatchingViewException diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/ManageOfflineContentPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/ManageOfflineContentPage.kt index 1af690a3dd..a15de9f163 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/ManageOfflineContentPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.ui.pages.offline +package com.instructure.student.ui.pages.classic.offline import androidx.test.espresso.Espresso import androidx.test.espresso.contrib.RecyclerViewActions diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/NativeDiscussionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/NativeDiscussionDetailsPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/NativeDiscussionDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/NativeDiscussionDetailsPage.kt index 8cc3233183..073580ce53 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/NativeDiscussionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/NativeDiscussionDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.offline +package com.instructure.student.ui.pages.classic.offline import android.os.SystemClock.sleep import androidx.test.espresso.Espresso @@ -51,7 +51,7 @@ import com.instructure.espresso.page.withId import com.instructure.espresso.page.withText import com.instructure.espresso.scrollTo import com.instructure.student.R -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.TypeInRCETextEditor import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.containsString diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/OfflineSyncSettingsPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/OfflineSyncSettingsPage.kt index d27445850e..46060f966d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/OfflineSyncSettingsPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.ui.pages.offline +package com.instructure.student.ui.pages.classic.offline import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isChecked diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/SyncProgressPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/SyncProgressPage.kt index dfb0cd3307..35e251c694 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/SyncProgressPage.kt @@ -15,12 +15,13 @@ * */ -package com.instructure.student.ui.pages.offline +package com.instructure.student.ui.pages.classic.offline import android.widget.TextView import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.hasSibling import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.canvas.espresso.getView import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertContainsText import com.instructure.espresso.assertDisplayed @@ -35,7 +36,6 @@ import com.instructure.espresso.page.withId import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.pandautils.R -import com.instructure.student.ui.utils.getView class SyncProgressPage : BasePage(R.id.syncProgressPage) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceDetailsRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceDetailsRenderTest.kt index f6d6478171..652e7889f6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceDetailsRenderTest.kt @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Conference import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsRepositoryFragment import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsViewState import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceRecordingViewState +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceListRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceListRenderTest.kt index 794fd1fcef..4f46583e3e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceListRenderTest.kt @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import android.graphics.Color import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListItemViewState import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListViewState +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/DiscussionSubmissionViewRenderTest.kt similarity index 91% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/DiscussionSubmissionViewRenderTest.kt index b76a80cf54..ef4aaa124a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/DiscussionSubmissionViewRenderTest.kt @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.DiscussionSubmissionViewFragment -import com.instructure.student.ui.pages.renderPages.DiscussionSubmissionViewRenderPage +import com.instructure.student.ui.rendertests.renderpages.DiscussionSubmissionViewRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/MediaSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/MediaSubmissionViewRenderTest.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/MediaSubmissionViewRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/MediaSubmissionViewRenderTest.kt index 7444707979..bc97fd33d8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/MediaSubmissionViewRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/MediaSubmissionViewRenderTest.kt @@ -14,19 +14,19 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import android.net.Uri import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.espresso.assertDisplayed import com.instructure.espresso.assertNotDisplayed import com.instructure.espresso.click import com.instructure.pandautils.utils.PandaPrefs -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsContentType import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.MediaSubmissionViewFragment -import com.instructure.student.ui.pages.renderPages.MediaSubmissionViewRenderPage +import com.instructure.student.ui.rendertests.renderpages.MediaSubmissionViewRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import org.junit.runner.RunWith diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PairObserverRenderTest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PairObserverRenderTest.kt index fb226e6cbb..241a25db98 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PairObserverRenderTest.kt @@ -14,12 +14,12 @@ * limitations under the License. * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.settings.pairobserver.PairObserverModel import com.instructure.student.mobius.settings.pairobserver.ui.PairObserverFragment +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PickerSubmissionUploadRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PickerSubmissionUploadRenderTest.kt index b1a7a71639..18127807f0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PickerSubmissionUploadRenderTest.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -29,13 +29,13 @@ import com.instructure.espresso.assertHasText import com.instructure.espresso.assertNotDisplayed import com.instructure.espresso.assertVisible import com.instructure.student.R -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerListItemViewState import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadFragment import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadViewState import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerVisibilities -import com.instructure.student.ui.pages.renderPages.PickerSubmissionUploadRenderPage +import com.instructure.student.ui.rendertests.renderpages.PickerSubmissionUploadRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/QuizSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/QuizSubmissionViewRenderTest.kt similarity index 90% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/QuizSubmissionViewRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/QuizSubmissionViewRenderTest.kt index 30deffe789..1dbd51ad11 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/QuizSubmissionViewRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/QuizSubmissionViewRenderTest.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import com.instructure.espresso.assertNotDisplayed import com.instructure.espresso.page.onViewWithId import com.instructure.student.R -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.QuizSubmissionViewFragment -import com.instructure.student.ui.pages.renderPages.QuizSubmissionViewRenderPage +import com.instructure.student.ui.rendertests.renderpages.QuizSubmissionViewRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionCommentsRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionCommentsRenderTest.kt index 10fbf51a03..87288d27ab 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionCommentsRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.espresso.Espresso import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -26,14 +26,14 @@ import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.User -import com.instructure.student.espresso.StudentRenderTest +import com.instructure.pandautils.room.studentdb.StudentDb +import com.instructure.pandautils.room.studentdb.entities.CreatePendingSubmissionCommentEntity import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.CommentItemState import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsViewState import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.ui.SubmissionCommentsFragment import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsTabData -import com.instructure.pandautils.room.studentdb.StudentDb -import com.instructure.pandautils.room.studentdb.entities.CreatePendingSubmissionCommentEntity -import com.instructure.student.ui.pages.renderPages.SubmissionCommentsRenderPage +import com.instructure.student.ui.rendertests.renderpages.SubmissionCommentsRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import junit.framework.Assert.assertTrue diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsEmptyContentRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsEmptyContentRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsEmptyContentRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsEmptyContentRenderTest.kt index 40d39e788b..5f4a82a5b5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsEmptyContentRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsEmptyContentRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.Assignment @@ -21,9 +21,9 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.LockInfo import com.instructure.canvasapi2.models.LockedModule import com.instructure.student.R -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.SubmissionDetailsEmptyContentModel import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.ui.SubmissionDetailsEmptyContentFragment +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsRenderTest.kt index ad15b6f54a..e87073ff76 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsRenderTest.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import android.content.pm.ActivityInfo import androidx.test.espresso.action.GeneralLocation import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Submission @@ -27,9 +27,9 @@ import com.instructure.canvasapi2.utils.DateHelper import com.instructure.espresso.assertGone import com.instructure.espresso.assertVisible import com.instructure.espresso.click -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsModel import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsRepositoryFragment +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionFilesRenderTest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionFilesRenderTest.kt index ab45b21fbb..70ff8ed801 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionFilesRenderTest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import android.graphics.Color import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -23,12 +23,12 @@ import com.instructure.espresso.assertGone import com.instructure.espresso.assertHasText import com.instructure.espresso.assertVisible import com.instructure.student.R -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.SubmissionFileData import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.SubmissionFilesViewState import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.ui.SubmissionFilesFragment import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsTabData -import com.instructure.student.ui.pages.renderPages.SubmissionFilesRenderPage +import com.instructure.student.ui.rendertests.renderpages.SubmissionFilesRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionRubricRenderTest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionRubricRenderTest.kt index 654d0437b3..e9c3e63113 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionRubricRenderTest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import android.graphics.Color import androidx.test.espresso.assertion.ViewAssertions.matches @@ -22,6 +22,7 @@ import androidx.test.espresso.matcher.RootMatchers import androidx.test.espresso.matcher.ViewMatchers.hasChildCount import androidx.test.espresso.matcher.ViewMatchers.isSelected import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvas.espresso.assertFontSizeSP import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.RubricCriterion import com.instructure.canvasapi2.models.RubricCriterionRating @@ -34,15 +35,14 @@ import com.instructure.espresso.assertVisible import com.instructure.espresso.click import com.instructure.espresso.page.onViewWithText import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellViewState -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.RatingData import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.RubricListData import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.SubmissionRubricModel import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.SubmissionRubricViewState import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.ui.SubmissionRubricFragment import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsTabData -import com.instructure.student.ui.pages.renderPages.SubmissionRubricRenderPage -import com.instructure.student.ui.utils.assertFontSizeSP +import com.instructure.student.ui.rendertests.renderpages.SubmissionRubricRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.CoreMatchers.not diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SyllabusRenderTest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SyllabusRenderTest.kt index 8c56f5169f..3a43da48eb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SyllabusRenderTest.kt @@ -14,16 +14,16 @@ * limitations under the License. * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.syllabus.SyllabusModel import com.instructure.student.mobius.syllabus.ui.SyllabusRepositoryFragment +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionUploadRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionUploadRenderTest.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionUploadRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionUploadRenderTest.kt index afe3a91efa..254985fa9d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionUploadRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionUploadRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers @@ -21,9 +21,9 @@ import androidx.test.espresso.matcher.ViewMatchers.isEnabled import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Course import com.instructure.espresso.waitForCheck -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submission.text.ui.TextSubmissionUploadFragment -import com.instructure.student.ui.pages.renderPages.TextSubmissionUploadRenderPage +import com.instructure.student.ui.rendertests.renderpages.TextSubmissionUploadRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.CoreMatchers.not import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionViewRenderTest.kt similarity index 89% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionViewRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionViewRenderTest.kt index 2ebbc05737..143aa728de 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionViewRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionViewRenderTest.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests -import com.instructure.canvas.espresso.Stub -import com.instructure.student.espresso.StudentRenderTest +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.TextSubmissionViewFragment -import com.instructure.student.ui.pages.renderPages.TextSubmissionViewRenderPage +import com.instructure.student.ui.rendertests.renderpages.TextSubmissionViewRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UploadStatusSubmissionRenderTest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UploadStatusSubmissionRenderTest.kt index 24522803ee..1f12a43d2b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UploadStatusSubmissionRenderTest.kt @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.espresso.StudentRenderTest +import com.instructure.pandautils.room.studentdb.entities.CreateFileSubmissionEntity import com.instructure.student.mobius.assignmentDetails.submission.file.UploadStatusSubmissionModel import com.instructure.student.mobius.assignmentDetails.submission.file.ui.UploadStatusSubmissionFragment -import com.instructure.pandautils.room.studentdb.entities.CreateFileSubmissionEntity +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionUploadRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionUploadRenderTest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionUploadRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionUploadRenderTest.kt index 4b1890beff..d10cccecb9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionUploadRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionUploadRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isEnabled @@ -23,8 +23,8 @@ import com.instructure.canvasapi2.models.Course import com.instructure.espresso.assertHasText import com.instructure.espresso.replaceText import com.instructure.espresso.waitForCheck -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submission.url.ui.UrlSubmissionUploadFragment +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers.not import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionViewRenderTest.kt similarity index 93% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionViewRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionViewRenderTest.kt index bdf618797a..d032231807 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionViewRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionViewRenderTest.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import com.instructure.espresso.assertCompletelyDisplayed import com.instructure.espresso.assertHasText -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.UrlSubmissionViewFragment +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceDetailsRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceDetailsRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceDetailsRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceDetailsRenderPage.kt index a93d63960a..4a58351281 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceDetailsRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceDetailsRenderPage.kt @@ -14,10 +14,11 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withAlpha +import com.instructure.canvas.espresso.assertIsRefreshing import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.assertGone @@ -30,8 +31,7 @@ import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.student.R import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceRecordingViewState -import com.instructure.student.ui.pages.ConferenceDetailsPage -import com.instructure.student.ui.utils.assertIsRefreshing +import com.instructure.student.ui.pages.classic.ConferenceDetailsPage import org.hamcrest.Matchers.allOf class ConferenceDetailsRenderPage : ConferenceDetailsPage() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceListRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceListRenderPage.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceListRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceListRenderPage.kt index 89c9667c14..afe553df23 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceListRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceListRenderPage.kt @@ -14,9 +14,10 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import com.instructure.canvas.espresso.assertIsRefreshing import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed @@ -31,8 +32,7 @@ import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.student.R import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListItemViewState -import com.instructure.student.ui.pages.ConferenceListPage -import com.instructure.student.ui.utils.assertIsRefreshing +import com.instructure.student.ui.pages.classic.ConferenceListPage import org.hamcrest.Matchers.allOf class ConferenceListRenderPage : ConferenceListPage() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/DiscussionSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/DiscussionSubmissionViewRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/DiscussionSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/DiscussionSubmissionViewRenderPage.kt index 4b871dac3e..c3d73f6b13 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/DiscussionSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/DiscussionSubmissionViewRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/MediaSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/MediaSubmissionViewRenderPage.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/MediaSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/MediaSubmissionViewRenderPage.kt index e5b4063b04..ea76599b8d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/MediaSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/MediaSubmissionViewRenderPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.WaitForViewWithId diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PairObserverRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PairObserverRenderPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PairObserverRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PairObserverRenderPage.kt index a008bce279..c08fb787ce 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PairObserverRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PairObserverRenderPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.espresso.OnViewWithId @@ -25,7 +25,7 @@ import com.instructure.espresso.page.withId import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.student.R -import com.instructure.student.ui.pages.PairObserverPage +import com.instructure.student.ui.pages.classic.PairObserverPage import org.hamcrest.Matchers.allOf class PairObserverRenderPage : PairObserverPage() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PickerSubmissionUploadRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PickerSubmissionUploadRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PickerSubmissionUploadRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PickerSubmissionUploadRenderPage.kt index c75c41a01a..7a3d2cf467 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PickerSubmissionUploadRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PickerSubmissionUploadRenderPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.hasDescendant diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/QuizSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/QuizSubmissionViewRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/QuizSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/QuizSubmissionViewRenderPage.kt index d1f7f0422c..a324aa4825 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/QuizSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/QuizSubmissionViewRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionCommentsRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionCommentsRenderPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionCommentsRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionCommentsRenderPage.kt index 394559aabc..c6ece490cc 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionCommentsRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionCommentsRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import android.os.SystemClock.sleep import android.view.View diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsEmptyContentRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsEmptyContentRenderPage.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsEmptyContentRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsEmptyContentRenderPage.kt index f0814650f1..d1ddee1f6b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsEmptyContentRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsEmptyContentRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.annotation.StringRes import androidx.test.espresso.assertion.ViewAssertions.matches @@ -23,7 +23,7 @@ import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertContainsText import com.instructure.espresso.assertHasText import com.instructure.student.R -import com.instructure.student.ui.pages.SubmissionDetailsEmptyContentPage +import com.instructure.student.ui.pages.classic.SubmissionDetailsEmptyContentPage import org.hamcrest.CoreMatchers.not class SubmissionDetailsEmptyContentRenderPage : SubmissionDetailsEmptyContentPage() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsRenderPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsRenderPage.kt index 0328828a1f..b07b53c8f7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsRenderPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import android.view.View import androidx.test.espresso.Espresso.onData @@ -42,7 +42,7 @@ import com.instructure.espresso.page.withText import com.instructure.espresso.scrollTo import com.instructure.espresso.waitForCheck import com.instructure.student.R -import com.instructure.student.ui.pages.SubmissionDetailsPage +import com.instructure.student.ui.pages.classic.SubmissionDetailsPage import com.sothree.slidinguppanel.SlidingUpPanelLayout import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anything diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionFilesRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionFilesRenderPage.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionFilesRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionFilesRenderPage.kt index 8be16aa0a4..338cbce7a0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionFilesRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionFilesRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.page.BasePage diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionRubricRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionRubricRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionRubricRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionRubricRenderPage.kt index 27f746ba57..d69c77ddcd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionRubricRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionRubricRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.page.BasePage diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SyllabusRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SyllabusRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SyllabusRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SyllabusRenderPage.kt index 8856191098..f35ebeb791 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SyllabusRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SyllabusRenderPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.action.ViewActions.swipeLeft import androidx.test.espresso.action.ViewActions.swipeRight @@ -34,7 +34,7 @@ import com.instructure.espresso.page.withId import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.student.R -import com.instructure.student.ui.pages.SyllabusPage +import com.instructure.student.ui.pages.classic.SyllabusPage import org.hamcrest.CoreMatchers import org.hamcrest.Matchers import org.hamcrest.Matchers.allOf diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionUploadRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionUploadRenderPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionUploadRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionUploadRenderPage.kt index 715613fc4f..069e47cafb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionUploadRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionUploadRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.page.BasePage diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionViewRenderPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionViewRenderPage.kt index 0d5dc42173..72465dd6f8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionViewRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UploadStatusSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UploadStatusSubmissionViewRenderPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UploadStatusSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UploadStatusSubmissionViewRenderPage.kt index 81f50e6a04..b4ca54ecc2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UploadStatusSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UploadStatusSubmissionViewRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertVisible diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionUploadRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionUploadRenderPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionUploadRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionUploadRenderPage.kt index 2aea428816..0fa583025c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionUploadRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionUploadRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.page.BasePage diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionViewRenderPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionViewRenderPage.kt index a550e30f6d..06b12150ef 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionViewRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.OnViewWithText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/views/GradeCellRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/views/GradeCellRenderTest.kt index 17151a2d39..64e5ddc10b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/views/GradeCellRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests.views +package com.instructure.student.ui.rendertests.renderpages.views import android.view.ViewGroup import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -26,7 +26,7 @@ import com.instructure.espresso.page.BasePage import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellView import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellViewState import com.instructure.student.R -import com.instructure.student.espresso.StudentRenderTest +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import org.junit.runner.RunWith diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/Matchers.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/Matchers.kt deleted file mode 100644 index d388442cfc..0000000000 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/Matchers.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2019 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.instructure.student.ui.utils - -import android.content.Intent -import android.content.res.Resources -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.drawable.Drawable -import android.view.View -import android.widget.ImageView -import android.widget.TextView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import androidx.test.espresso.UiController -import androidx.test.espresso.ViewAction -import androidx.test.espresso.ViewInteraction -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.BoundedMatcher -import androidx.test.espresso.matcher.ViewMatchers.assertThat -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom -import com.instructure.pandautils.utils.ColorUtils -import org.hamcrest.CoreMatchers.`is` -import org.hamcrest.Description -import org.hamcrest.Matcher -import org.hamcrest.TypeSafeMatcher - -fun ViewInteraction.assertLineCount(lineCount: Int) { - val matcher = object : TypeSafeMatcher() { - override fun matchesSafely(item: View): Boolean { - return (item as TextView).lineCount == lineCount - } - - override fun describeTo(description: Description) { - description.appendText("isTextInLines") - } - } - check(matches(matcher)) -} - - -fun ViewInteraction.getView(): View { - lateinit var matchingView: View - perform(object : ViewAction { - override fun getDescription() = "Get View reference" - - override fun getConstraints(): Matcher { - return isAssignableFrom(View::class.java) - } - - override fun perform(uiController: UiController?, view: View) { - matchingView = view - } - }) - return matchingView -} - -fun ViewInteraction.assertCompletelyAbove(other: ViewInteraction) { - val view1 = getView() - val view2 = other.getView() - val location1 = view1.locationOnScreen - val location2 = view2.locationOnScreen - val isAbove = location1[1] + view1.height <= location2[1] - assertThat("completely above", isAbove, `is`(true)) -} - -fun ViewInteraction.assertCompletelyBelow(other: ViewInteraction) { - val view1 = getView() - val view2 = other.getView() - val location1 = view1.locationOnScreen - val location2 = view2.locationOnScreen - val isAbove = location2[1] + view2.height <= location1[1] - assertThat("completely below", isAbove, `is`(true)) -} - -val View.locationOnScreen get() = IntArray(2).apply { getLocationOnScreen(this) } - - -/** - * Asserts that the TextView uses the specified font size in scaled pixels - */ -fun ViewInteraction.assertFontSizeSP(expectedSP: Float) { - val matcher = object : TypeSafeMatcher(View::class.java) { - - override fun matchesSafely(target: View): Boolean { - if (target !is TextView) return false - val actualSP = target.textSize / target.getResources().displayMetrics.scaledDensity - return actualSP.compareTo(expectedSP) == 0 - } - - override fun describeTo(description: Description) { - description.appendText("with fontSize: ${expectedSP}px") - } - } - check(matches(matcher)) -} - -fun ViewInteraction.assertIsRefreshing(isRefreshing: Boolean) { - val matcher = object : BoundedMatcher(SwipeRefreshLayout::class.java) { - - override fun describeTo(description: Description) { - description.appendText(if (isRefreshing) "is refreshing" else "is not refreshing") - } - - override fun matchesSafely(view: SwipeRefreshLayout): Boolean { - return view.isRefreshing == isRefreshing - } - } - check(matches(matcher)) -} - -class IntentActionMatcher(private val intentType: String, private val dataMatcher: String) : TypeSafeMatcher() { - - override fun describeTo(description: Description?) { - description?.appendText("Intent Matcher") - } - - override fun matchesSafely(item: Intent?): Boolean { - return (intentType == item?.action) && (item?.dataString?.contains(dataMatcher) ?: false) - } -} - -// Adapted from https://medium.com/@dbottillo/android-ui-test-espresso-matcher-for-imageview-1a28c832626f -/** - * Matches ImageView (or ImageButton) with the drawable associated with [resourceId]. If [resourceId] < 0, will - * match against "no drawable" / "drawable is null". - * - * If the [color] param is non-null, then the drawable associated with [resourceId] will be colored - * prior to matching. - */ -class ImageViewDrawableMatcher(val resourceId: Int, val color: Int? = null) : TypeSafeMatcher( - ImageView::class.java) { - override fun describeTo(description: Description) { - description.appendText("with drawable from resource id: ") - description.appendValue(resourceId) - } - - override fun matchesSafely(target: View?): Boolean { - if (target !is ImageView) { - return false - } - val imageView = target - if (resourceId < 0) { - return imageView.drawable == null - } - val resources: Resources = target.getContext().getResources() - val expectedDrawable: Drawable = resources.getDrawable(resourceId) ?: return false - if(color != null) { - ColorUtils.colorIt(color, expectedDrawable) - } - val bitmap: Bitmap = getBitmap(imageView.getDrawable()) - val otherBitmap: Bitmap = getBitmap(expectedDrawable) - return bitmap.sameAs(otherBitmap) - } - - private fun getBitmap(drawable: Drawable): Bitmap { - val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, - drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()) - drawable.draw(canvas) - return bitmap - } -} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt index 4350c4c16a..3c3b86c9a6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt @@ -19,7 +19,7 @@ package com.instructure.student.ui.utils import androidx.compose.ui.test.junit4.createAndroidComposeRule -import com.instructure.canvas.espresso.common.pages.ReminderPage +import com.instructure.canvas.espresso.common.pages.AssignmentReminderPage import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventCreateEditPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage @@ -51,7 +51,7 @@ abstract class StudentComposeTest : StudentTest() { val calendarToDoDetailsPage = CalendarToDoDetailsPage(composeTestRule) val calendarFilterPage = CalendarFilterPage(composeTestRule) val settingsPage = SettingsPage(composeTestRule) - val reminderPage = ReminderPage(composeTestRule) + val assignmentReminderPage = AssignmentReminderPage(composeTestRule) val inboxDetailsPage = InboxDetailsPage(composeTestRule) val inboxComposePage = InboxComposePage(composeTestRule) val recipientPickerPage = RecipientPickerPage(composeTestRule) diff --git a/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentRenderTest.kt similarity index 67% rename from apps/student/src/androidTest/java/com/instructure/student/espresso/StudentRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentRenderTest.kt index ca812241d3..10fddb2657 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentRenderTest.kt @@ -14,13 +14,20 @@ * limitations under the License. * */ -package com.instructure.student.espresso +package com.instructure.student.ui.utils import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.student.SingleFragmentTestActivity -import com.instructure.student.ui.pages.renderPages.* -import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.rendertests.renderpages.ConferenceDetailsRenderPage +import com.instructure.student.ui.rendertests.renderpages.ConferenceListRenderPage +import com.instructure.student.ui.rendertests.renderpages.PairObserverRenderPage +import com.instructure.student.ui.rendertests.renderpages.SubmissionDetailsEmptyContentRenderPage +import com.instructure.student.ui.rendertests.renderpages.SubmissionDetailsRenderPage +import com.instructure.student.ui.rendertests.renderpages.SyllabusRenderPage +import com.instructure.student.ui.rendertests.renderpages.TextSubmissionUploadRenderPage +import com.instructure.student.ui.rendertests.renderpages.UploadStatusSubmissionViewRenderPage +import com.instructure.student.ui.rendertests.renderpages.UrlSubmissionUploadRenderPage +import com.instructure.student.ui.rendertests.renderpages.UrlSubmissionViewRenderPage import org.junit.runner.RunWith // Test from which all Student PageRender/SingleFragmentTestActivity tests will derive diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index 598f9465ef..ecad0fc8e0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -48,58 +48,59 @@ import com.instructure.pandautils.utils.Const import com.instructure.student.BuildConfig import com.instructure.student.R import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.AllCoursesPage -import com.instructure.student.ui.pages.AnnotationCommentListPage -import com.instructure.student.ui.pages.AnnouncementListPage -import com.instructure.student.ui.pages.BookmarkPage -import com.instructure.student.ui.pages.CanvasWebViewPage -import com.instructure.student.ui.pages.ConferenceDetailsPage -import com.instructure.student.ui.pages.ConferenceListPage -import com.instructure.student.ui.pages.CourseBrowserPage -import com.instructure.student.ui.pages.CourseGradesPage -import com.instructure.student.ui.pages.DashboardPage -import com.instructure.student.ui.pages.DiscussionListPage -import com.instructure.student.ui.pages.ElementaryCoursePage -import com.instructure.student.ui.pages.ElementaryDashboardPage -import com.instructure.student.ui.pages.FileChooserPage -import com.instructure.student.ui.pages.FileListPage -import com.instructure.student.ui.pages.GoToQuizPage -import com.instructure.student.ui.pages.GradesPage -import com.instructure.student.ui.pages.GroupBrowserPage -import com.instructure.student.ui.pages.HelpPage -import com.instructure.student.ui.pages.HomeroomPage -import com.instructure.student.ui.pages.ImportantDatesPage -import com.instructure.student.ui.pages.LeftSideNavigationDrawerPage -import com.instructure.student.ui.pages.ModuleProgressionPage -import com.instructure.student.ui.pages.ModulesPage -import com.instructure.student.ui.pages.NotificationPage -import com.instructure.student.ui.pages.PageDetailsPage -import com.instructure.student.ui.pages.PageListPage -import com.instructure.student.ui.pages.PairObserverPage -import com.instructure.student.ui.pages.PandaAvatarPage -import com.instructure.student.ui.pages.PeopleListPage -import com.instructure.student.ui.pages.PersonDetailsPage -import com.instructure.student.ui.pages.PickerSubmissionUploadPage -import com.instructure.student.ui.pages.ProfileSettingsPage -import com.instructure.student.ui.pages.PushNotificationsPage -import com.instructure.student.ui.pages.QRLoginPage -import com.instructure.student.ui.pages.QuizListPage -import com.instructure.student.ui.pages.QuizTakingPage -import com.instructure.student.ui.pages.RemoteConfigSettingsPage -import com.instructure.student.ui.pages.ResourcesPage -import com.instructure.student.ui.pages.SchedulePage -import com.instructure.student.ui.pages.ShareExtensionStatusPage -import com.instructure.student.ui.pages.ShareExtensionTargetPage -import com.instructure.student.ui.pages.StudentAssignmentDetailsPage -import com.instructure.student.ui.pages.SubmissionDetailsPage -import com.instructure.student.ui.pages.SyllabusPage -import com.instructure.student.ui.pages.TextSubmissionUploadPage -import com.instructure.student.ui.pages.TodoPage -import com.instructure.student.ui.pages.UrlSubmissionUploadPage -import com.instructure.student.ui.pages.offline.ManageOfflineContentPage -import com.instructure.student.ui.pages.offline.NativeDiscussionDetailsPage -import com.instructure.student.ui.pages.offline.OfflineSyncSettingsPage -import com.instructure.student.ui.pages.offline.SyncProgressPage +import com.instructure.student.ui.pages.classic.AllCoursesPage +import com.instructure.student.ui.pages.classic.AnnotationCommentListPage +import com.instructure.student.ui.pages.classic.AnnouncementListPage +import com.instructure.student.ui.pages.classic.BookmarkPage +import com.instructure.student.ui.pages.classic.CanvasWebViewPage +import com.instructure.student.ui.pages.classic.ConferenceDetailsPage +import com.instructure.student.ui.pages.classic.ConferenceListPage +import com.instructure.student.ui.pages.classic.CourseBrowserPage +import com.instructure.student.ui.pages.classic.CourseGradesPage +import com.instructure.student.ui.pages.classic.DashboardPage +import com.instructure.student.ui.pages.classic.DiscussionDetailsPage +import com.instructure.student.ui.pages.classic.DiscussionListPage +import com.instructure.student.ui.pages.classic.FileChooserPage +import com.instructure.student.ui.pages.classic.FileListPage +import com.instructure.student.ui.pages.classic.GoToQuizPage +import com.instructure.student.ui.pages.classic.GradesPage +import com.instructure.student.ui.pages.classic.GroupBrowserPage +import com.instructure.student.ui.pages.classic.HelpPage +import com.instructure.student.ui.pages.classic.LeftSideNavigationDrawerPage +import com.instructure.student.ui.pages.classic.ModuleProgressionPage +import com.instructure.student.ui.pages.classic.ModulesPage +import com.instructure.student.ui.pages.classic.NotificationPage +import com.instructure.student.ui.pages.classic.PageDetailsPage +import com.instructure.student.ui.pages.classic.PageListPage +import com.instructure.student.ui.pages.classic.PairObserverPage +import com.instructure.student.ui.pages.classic.PandaAvatarPage +import com.instructure.student.ui.pages.classic.PeopleListPage +import com.instructure.student.ui.pages.classic.PersonDetailsPage +import com.instructure.student.ui.pages.classic.PickerSubmissionUploadPage +import com.instructure.student.ui.pages.classic.ProfileSettingsPage +import com.instructure.student.ui.pages.classic.PushNotificationsPage +import com.instructure.student.ui.pages.classic.QRLoginPage +import com.instructure.student.ui.pages.classic.QuizListPage +import com.instructure.student.ui.pages.classic.QuizTakingPage +import com.instructure.student.ui.pages.classic.RemoteConfigSettingsPage +import com.instructure.student.ui.pages.classic.ShareExtensionStatusPage +import com.instructure.student.ui.pages.classic.ShareExtensionTargetPage +import com.instructure.student.ui.pages.classic.StudentAssignmentDetailsPage +import com.instructure.student.ui.pages.classic.SubmissionDetailsPage +import com.instructure.student.ui.pages.classic.SyllabusPage +import com.instructure.student.ui.pages.classic.TextSubmissionUploadPage +import com.instructure.student.ui.pages.classic.TodoPage +import com.instructure.student.ui.pages.classic.UrlSubmissionUploadPage +import com.instructure.student.ui.pages.classic.k5.ElementaryCoursePage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.HomeroomPage +import com.instructure.student.ui.pages.classic.k5.ImportantDatesPage +import com.instructure.student.ui.pages.classic.k5.ResourcesPage +import com.instructure.student.ui.pages.classic.k5.SchedulePage +import com.instructure.student.ui.pages.classic.offline.ManageOfflineContentPage +import com.instructure.student.ui.pages.classic.offline.NativeDiscussionDetailsPage +import com.instructure.student.ui.pages.classic.offline.OfflineSyncSettingsPage +import com.instructure.student.ui.pages.classic.offline.SyncProgressPage import instructure.rceditor.RCETextEditor import org.hamcrest.Matcher import org.hamcrest.core.AllOf @@ -130,7 +131,7 @@ abstract class StudentTest : CanvasTest() { val courseGradesPage = CourseGradesPage() val dashboardPage = DashboardPage() val leftSideNavigationDrawerPage = LeftSideNavigationDrawerPage() - val discussionDetailsPage = com.instructure.student.ui.pages.DiscussionDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) + val discussionDetailsPage = DiscussionDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) val nativeDiscussionDetailsPage = NativeDiscussionDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) val discussionListPage = DiscussionListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn)) val allCoursesPage = AllCoursesPage() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/ViewUtils.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/ViewUtils.kt deleted file mode 100644 index 80e0d0a305..0000000000 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/ViewUtils.kt +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (C) 2018-present Instructure, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -package com.instructure.student.ui.utils - -import android.view.View -import androidx.test.espresso.Espresso -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions -import org.hamcrest.Matcher - -object ViewUtils { - - fun pressBackButton(times: Int) { - for(i in 1..times) { - Espresso.pressBack() - } - } - - fun waitForViewToDisappear(viewMatcher: Matcher, timeoutInSeconds: Long) { - val startTime = System.currentTimeMillis() - - while (System.currentTimeMillis() - startTime < (timeoutInSeconds * 1000)) { - try { - onView(viewMatcher) - .check(ViewAssertions.doesNotExist()) - return - } catch (e: AssertionError) { - Thread.sleep(200) - } - } - throw AssertionError("The view has not been displayed within $timeoutInSeconds seconds.") - } -} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/extensions/StudentTestExtensions.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/utils/extensions/StudentTestExtensions.kt index 10691bb62d..1f74691925 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/extensions/StudentTestExtensions.kt @@ -16,7 +16,7 @@ */ @file:Suppress("unused") -package com.instructure.student.ui.utils +package com.instructure.student.ui.utils.extensions import android.app.Activity import android.content.Intent @@ -54,6 +54,7 @@ import com.instructure.interactions.router.Route import com.instructure.student.R import com.instructure.student.activity.LoginActivity import com.instructure.student.router.RouteMatcher +import com.instructure.student.ui.utils.StudentTest import java.io.File import java.io.FileWriter diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/offline/OfflineTestUtils.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/utils/offline/OfflineTestUtils.kt index 9ad81df4b4..ae58424527 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/offline/OfflineTestUtils.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline.utils +package com.instructure.student.ui.utils.offline import androidx.test.espresso.Espresso.onView import androidx.test.espresso.matcher.ViewMatchers.hasDescendant diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle index e876ef6541..94405a5a01 100644 --- a/apps/teacher/build.gradle +++ b/apps/teacher/build.gradle @@ -38,7 +38,7 @@ android { versionCode = 82 versionName = '2.0.1' vectorDrawables.useSupportLibrary = true - testInstrumentationRunner 'com.instructure.teacher.ui.espresso.TeacherHiltTestRunner' + testInstrumentationRunner 'com.instructure.teacher.espresso.TeacherHiltTestRunner' testInstrumentationRunnerArguments disableAnalytics: 'true' /* BuildConfig fields */ diff --git a/apps/teacher/flank.yml b/apps/teacher/flank.yml index eafa4e03e4..a252da921a 100644 --- a/apps/teacher/flank.yml +++ b/apps/teacher/flank.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: Pixel2.arm version: 29 diff --git a/apps/teacher/flank_coverage.yml b/apps/teacher/flank_coverage.yml index 3d23a78f48..d5415d3b04 100644 --- a/apps/teacher/flank_coverage.yml +++ b/apps/teacher/flank_coverage.yml @@ -19,7 +19,7 @@ gcloud: directories-to-pull: - /sdcard/ test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubCoverage device: - model: Pixel2.arm version: 29 diff --git a/apps/teacher/flank_e2e.yml b/apps/teacher/flank_e2e.yml index 3734162948..eb5c5c77f7 100644 --- a/apps/teacher/flank_e2e.yml +++ b/apps/teacher/flank_e2e.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: Pixel2.arm version: 29 diff --git a/apps/teacher/flank_e2e_coverage.yml b/apps/teacher/flank_e2e_coverage.yml index af3fe15179..17989f8cd2 100644 --- a/apps/teacher/flank_e2e_coverage.yml +++ b/apps/teacher/flank_e2e_coverage.yml @@ -19,8 +19,8 @@ gcloud: directories-to-pull: - /sdcard/ test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubCoverage device: - model: Pixel2.arm version: 29 diff --git a/apps/teacher/flank_e2e_flaky.yml b/apps/teacher/flank_e2e_flaky.yml index 00402815a8..2653bb332c 100644 --- a/apps/teacher/flank_e2e_flaky.yml +++ b/apps/teacher/flank_e2e_flaky.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.FlakyE2E + - annotation com.instructure.canvas.espresso.annotations.FlakyE2E device: - model: Nexus6P version: 26 diff --git a/apps/teacher/flank_e2e_knownbug.yml b/apps/teacher/flank_e2e_knownbug.yml index a8bee4b7e0..fb7386683b 100644 --- a/apps/teacher/flank_e2e_knownbug.yml +++ b/apps/teacher/flank_e2e_knownbug.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.KnownBug device: - model: Nexus6P version: 26 diff --git a/apps/teacher/flank_e2e_lowres.yml b/apps/teacher/flank_e2e_lowres.yml index b6e24c1447..d9fa4b0dc8 100644 --- a/apps/teacher/flank_e2e_lowres.yml +++ b/apps/teacher/flank_e2e_lowres.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: NexusLowRes version: 29 diff --git a/apps/teacher/flank_e2e_min.yml b/apps/teacher/flank_e2e_min.yml index 8137c96453..7ac548f08f 100644 --- a/apps/teacher/flank_e2e_min.yml +++ b/apps/teacher/flank_e2e_min.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: Nexus6P version: 26 diff --git a/apps/teacher/flank_landscape.yml b/apps/teacher/flank_landscape.yml index a2919c86ba..116542032a 100644 --- a/apps/teacher/flank_landscape.yml +++ b/apps/teacher/flank_landscape.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubLandscape + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubLandscape device: - model: Pixel2.arm version: 29 diff --git a/apps/teacher/flank_multi_api_level.yml b/apps/teacher/flank_multi_api_level.yml index 0b907030b7..282b5f0ee2 100644 --- a/apps/teacher/flank_multi_api_level.yml +++ b/apps/teacher/flank_multi_api_level.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubMultiAPILevel, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: NexusLowRes version: 27 diff --git a/apps/teacher/flank_tablet.yml b/apps/teacher/flank_tablet.yml index 6ec715a404..13224a18b7 100644 --- a/apps/teacher/flank_tablet.yml +++ b/apps/teacher/flank_tablet.yml @@ -9,7 +9,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubTablet + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubTablet device: - model: MediumTablet.arm version: 29 diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestApplication.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestApplication.kt similarity index 94% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestApplication.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestApplication.kt index 6a46e72ab0..78268ee0e9 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestApplication.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestApplication.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.teacher.ui.espresso +package com.instructure.teacher.espresso import dagger.hilt.android.testing.CustomTestApplication diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestRunner.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestRunner.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestRunner.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestRunner.kt index e24890c8e6..00f1d3b0f0 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestRunner.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestRunner.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.teacher.ui.espresso +package com.instructure.teacher.espresso import android.app.Application import android.content.Context diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TestAppManager.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TestAppManager.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TestAppManager.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TestAppManager.kt index 9193324451..fcc640a20e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TestAppManager.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TestAppManager.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.espresso +package com.instructure.teacher.espresso import androidx.work.WorkerFactory import com.instructure.pandautils.features.reminder.AlarmScheduler diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginFindSchoolPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginFindSchoolPageTest.kt deleted file mode 100644 index 48bdaf59c6..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginFindSchoolPageTest.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.instructure.teacher.ui - -import com.instructure.teacher.ui.utils.TeacherTest -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test - -@HiltAndroidTest -class LoginFindSchoolPageTest: TeacherTest() { - - @Test - override fun displaysPageObjects() { - loginLandingPage.clickFindMySchoolButton() - loginFindSchoolPage.assertPageObjects() - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginLandingPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginLandingPageTest.kt deleted file mode 100644 index 2a01e2422b..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginLandingPageTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.instructure.teacher.ui - -import com.instructure.espresso.filters.P1 -import com.instructure.teacher.ui.utils.TeacherTest -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test - -@HiltAndroidTest -class LoginLandingPageTest: TeacherTest() { - - // Runs live; no MockCanvas - @Test - @P1 - override fun displaysPageObjects() { - loginLandingPage.assertPageObjects() - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginSignInPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginSignInPageTest.kt deleted file mode 100644 index 2bdbcdd5d0..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginSignInPageTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2017 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.instructure.teacher.ui - -import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.enterDomain -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test - -@HiltAndroidTest -class LoginSignInPageTest: TeacherTest() { - - // Runs live; no MockCanvas - @Test - override fun displaysPageObjects() { - loginLandingPage.clickFindMySchoolButton() - enterDomain() - loginFindSchoolPage.clickToolbarNextMenuItem() - loginSignInPage.assertPageObjects() - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/AnnouncementsE2ETest.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/AnnouncementsE2ETest.kt index 13a2f04bf5..b842888adb 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/AnnouncementsE2ETest.kt @@ -14,17 +14,17 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CommentLibraryE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CommentLibraryE2ETest.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CommentLibraryE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CommentLibraryE2ETest.kt index 317d57b0f6..f9f6832391 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CommentLibraryE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CommentLibraryE2ETest.kt @@ -14,14 +14,14 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CommentLibraryApi import com.instructure.dataseeding.api.SubmissionsApi @@ -34,8 +34,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CourseSettingsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CourseSettingsE2ETest.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CourseSettingsE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CourseSettingsE2ETest.kt index 324510d742..74152321e0 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CourseSettingsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CourseSettingsE2ETest.kt @@ -14,19 +14,19 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.refresh import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DashboardE2ETest.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DashboardE2ETest.kt index f4e20a5206..671824b440 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DashboardE2ETest.kt @@ -14,18 +14,18 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DiscussionsE2ETest.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DiscussionsE2ETest.kt index 5f23ecf55a..9ef7a23056 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DiscussionsE2ETest.kt @@ -14,19 +14,19 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.lang.Thread.sleep diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/FilesE2ETest.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/FilesE2ETest.kt index cfbbab3973..49eea85991 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/FilesE2ETest.kt @@ -14,16 +14,17 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.os.Environment import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.managers.DiscussionManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.weave.awaitApiResponse @@ -35,11 +36,10 @@ import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.FileUploadType import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.Randomizer -import com.instructure.espresso.ViewUtils import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin -import com.instructure.teacher.ui.utils.uploadTextFile +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin +import com.instructure.teacher.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.io.File @@ -135,7 +135,7 @@ class FilesE2ETest: TeacherComposeTest() { fileListPage.assertItemDisplayed(discussionAttachmentFile.name) Log.d(STEP_TAG, "Navigate back to the Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open '${course.name}' course and navigate to Assignments Page.") dashboardPage.openCourse(course.name) @@ -159,7 +159,7 @@ class FilesE2ETest: TeacherComposeTest() { speedGraderPage.assertCommentAttachmentDisplayedCommon(commentUploadInfo.fileName, student.shortName) */ Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(5) + pressBackButton(5) Log.d(STEP_TAG, "Navigate to 'Files' menu in user left-side menu.") leftSideNavigationDrawerPage.clickFilesMenu() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/HelpMenuE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/HelpMenuE2ETest.kt similarity index 94% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/HelpMenuE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/HelpMenuE2ETest.kt index 7e0c77d58b..ba2df47f7e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/HelpMenuE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/HelpMenuE2ETest.kt @@ -14,21 +14,22 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.intent.Intents -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.checkToastText import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/LoginE2ETest.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/LoginE2ETest.kt index 3a31e212d4..773fde7d66 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/LoginE2ETest.kt @@ -14,25 +14,25 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.dataseeding.api.SeedApi import com.instructure.dataseeding.api.UserApi import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.CourseApiModel -import com.instructure.espresso.ViewUtils import com.instructure.espresso.withIdlingResourceDisabled import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData +import com.instructure.teacher.ui.utils.extensions.seedData import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -299,7 +299,7 @@ class LoginE2ETest : TeacherTest() { peopleListPage.assertPersonListed(user, role) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) } private fun assertSuccessfulLogin(user: CanvasUserApiModel) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/ModulesE2ETest.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/ModulesE2ETest.kt index 921e14ce55..27fc1f485d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/ModulesE2ETest.kt @@ -1,12 +1,12 @@ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.dataseeding.api.FileFolderApi @@ -23,10 +23,10 @@ import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.getCustomDateCalendar import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.openOverflowMenu -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin -import com.instructure.teacher.ui.utils.uploadTextFile +import com.instructure.teacher.ui.utils.extensions.openOverflowMenu +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin +import com.instructure.teacher.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PagesE2ETest.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PagesE2ETest.kt index 131c047baa..529a1b7863 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PagesE2ETest.kt @@ -1,18 +1,18 @@ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.PagesApi -import com.instructure.teacher.ui.pages.WebViewTextCheck +import com.instructure.teacher.ui.pages.classic.WebViewTextCheck import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PeopleE2ETest.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PeopleE2ETest.kt index 5064daa0a7..170841f22f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PeopleE2ETest.kt @@ -14,29 +14,29 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.GroupsApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils -import com.instructure.teacher.ui.pages.PeopleListPage -import com.instructure.teacher.ui.pages.PersonContextPage +import com.instructure.teacher.ui.pages.classic.PeopleListPage +import com.instructure.teacher.ui.pages.classic.PersonContextPage import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedAssignmentSubmission -import com.instructure.teacher.ui.utils.seedAssignments -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedAssignmentSubmission +import com.instructure.teacher.ui.utils.extensions.seedAssignments +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.lang.Thread.sleep @@ -187,7 +187,7 @@ class PeopleE2ETest: TeacherComposeTest() { peopleListPage.assertSearchResultCount(5) Log.d(STEP_TAG, "Quit from searching and navigate to People List page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Click on the 'Filter' icon on the top-right corner and select '${group.name}' group as a filter.") peopleListPage.clickOnPeopleFilterMenu() @@ -233,7 +233,7 @@ class PeopleE2ETest: TeacherComposeTest() { peopleListPage.assertSearchResultCount(5) Log.d(STEP_TAG, "Navigate back to Dashboard Page. Click on the Inbox bottom menu.") - ViewUtils.pressBackButton(2) + pressBackButton(2) dashboardPage.openInbox() Log.d(ASSERTION_TAG, "Assert that the Inbox is empty.") diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PushNotificationsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PushNotificationsE2ETest.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PushNotificationsE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PushNotificationsE2ETest.kt index a7acb1eacb..76062900e4 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PushNotificationsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PushNotificationsE2ETest.kt @@ -1,11 +1,11 @@ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.teacher.BuildConfig import com.instructure.teacher.ui.utils.TeacherComposeTest import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/QuizE2ETest.kt similarity index 93% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/QuizE2ETest.kt index 5944219546..7955ef3364 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/QuizE2ETest.kt @@ -14,24 +14,24 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.seedQuizQuestion -import com.instructure.teacher.ui.utils.seedQuizSubmission -import com.instructure.teacher.ui.utils.seedQuizzes -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.seedQuizQuestion +import com.instructure.teacher.ui.utils.extensions.seedQuizSubmission +import com.instructure.teacher.ui.utils.extensions.seedQuizzes +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SettingsE2ETest.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SettingsE2ETest.kt index a09ec7dcee..71cf81e941 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SettingsE2ETest.kt @@ -14,33 +14,33 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.NoMatchingViewException -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.utils.RemoteConfigParam import com.instructure.canvasapi2.utils.RemoteConfigUtils import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.dataseeding.util.CanvasNetworkAdapter -import com.instructure.espresso.ViewUtils import com.instructure.pandautils.utils.AppTheme import com.instructure.teacher.BuildConfig import com.instructure.teacher.R -import com.instructure.teacher.ui.pages.PersonContextPage +import com.instructure.teacher.ui.pages.classic.PersonContextPage import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.openLeftSideMenu -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.openLeftSideMenu +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -101,7 +101,7 @@ class SettingsE2ETest : TeacherComposeTest() { profileSettingsPage.assertPronouns(testPronoun) Log.d(STEP_TAG, "Navigate back to the Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Select '${course.name}' course and open 'People' tab.") dashboardPage.selectCourse(course) @@ -124,7 +124,7 @@ class SettingsE2ETest : TeacherComposeTest() { CoursesApi.concludeCourse(course.id) // Need to conclude the course because otherwise there would be too much course with time on the dedicated user's dashboard. Log.d(STEP_TAG, "Navigate back to Dashboard.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Open the Left Side Menu.") dashboardPage.openLeftSideMenu() @@ -482,7 +482,7 @@ class SettingsE2ETest : TeacherComposeTest() { inboxSignatureSettingsPage.assertSignatureEnabledState(true) Log.d(STEP_TAG, "Navigate back to the Dashboard.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open Inbox Page.") dashboardPage.openInbox() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SyllabusE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SyllabusE2ETest.kt similarity index 92% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SyllabusE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SyllabusE2ETest.kt index 267d08a824..935eceb8df 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SyllabusE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SyllabusE2ETest.kt @@ -1,7 +1,7 @@ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory @@ -11,10 +11,10 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedAssignments -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.seedQuizzes -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedAssignments +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.seedQuizzes +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/TodoE2ETest.kt similarity index 91% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/TodoE2ETest.kt index 8a6d6d214c..6c918e2bc8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/TodoE2ETest.kt @@ -14,10 +14,10 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory @@ -27,15 +27,15 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils +import com.instructure.canvas.espresso.pressBackButton import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedAssignmentSubmission -import com.instructure.teacher.ui.utils.seedAssignments -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.seedQuizQuestion -import com.instructure.teacher.ui.utils.seedQuizSubmission -import com.instructure.teacher.ui.utils.seedQuizzes -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedAssignmentSubmission +import com.instructure.teacher.ui.utils.extensions.seedAssignments +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.seedQuizQuestion +import com.instructure.teacher.ui.utils.extensions.seedQuizSubmission +import com.instructure.teacher.ui.utils.extensions.seedQuizzes +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -110,7 +110,7 @@ class TodoE2ETest : TeacherTest() { seedQuizSubmission(courseId = course.id, quizId = testQuiz.id, studentToken = student.token) Log.d(STEP_TAG, "Navigate back to the Dashboard Page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Navigate to 'To Do' Page.") dashboardPage.openTodo() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/AssignmentE2ETest.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/AssignmentE2ETest.kt index 2333961c33..b8eb90c7f2 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/AssignmentE2ETest.kt @@ -14,19 +14,19 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.compose import android.os.SystemClock.sleep import android.util.Log import androidx.test.espresso.Espresso import androidx.test.rule.GrantPermissionRule -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SectionsApi @@ -39,11 +39,11 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedAssignmentSubmission -import com.instructure.teacher.ui.utils.seedAssignments -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin -import com.instructure.teacher.ui.utils.uploadTextFile +import com.instructure.teacher.ui.utils.extensions.seedAssignmentSubmission +import com.instructure.teacher.ui.utils.extensions.seedAssignments +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin +import com.instructure.teacher.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Rule import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/CalendarE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/CalendarE2ETest.kt index 2c70758a65..dd77cfa54a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/CalendarE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/CalendarE2ETest.kt @@ -16,17 +16,17 @@ package com.instructure.teacher.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.espresso.getDateInCanvasCalendarFormat import com.instructure.pandautils.features.calendar.CalendarPrefs import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.clickCalendarTab -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.clickCalendarTab +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/InboxE2ETest.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/InboxE2ETest.kt index 1436c9cdb1..9b4e3d99f5 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/InboxE2ETest.kt @@ -1,14 +1,14 @@ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.ReleaseExclude import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.ReleaseExclude import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi @@ -17,8 +17,8 @@ import com.instructure.dataseeding.model.CourseApiModel import com.instructure.espresso.retry import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/SpeedGraderE2ETest.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/SpeedGraderE2ETest.kt index f490ada8d6..8f51c461f4 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/SpeedGraderE2ETest.kt @@ -14,32 +14,30 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.compose import android.util.Log -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.printToLog import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils import com.instructure.espresso.retry import com.instructure.teacher.R -import com.instructure.teacher.ui.pages.PersonContextPage +import com.instructure.teacher.ui.pages.classic.PersonContextPage import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedAssignmentSubmission -import com.instructure.teacher.ui.utils.seedAssignments -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedAssignmentSubmission +import com.instructure.teacher.ui.utils.extensions.seedAssignments +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -124,8 +122,6 @@ class SpeedGraderE2ETest : TeacherComposeTest() { assignmentSubmissionListPage.assertFilterLabelNotSubmittedSubmissions() assignmentSubmissionListPage.assertHasStudentSubmission(noSubStudent) - composeTestRule.onRoot().printToLog("SEMANTIC_TREE") - Log.d(ASSERTION_TAG, "Assert that the '${noSubStudent.name}' student has '-' as score as it's submission is not submitted yet.") assignmentSubmissionListPage.assertStudentScoreText(noSubStudent.name, "-") @@ -146,7 +142,7 @@ class SpeedGraderE2ETest : TeacherComposeTest() { studentContextPage.assertSectionNameView(PersonContextPage.UserRole.STUDENT) Log.d(STEP_TAG, "Navigate back to the Assignment Details Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open 'Graded' submissions.") assignmentDetailsPage.clickGradedSubmissions() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AnnouncementsListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AnnouncementsListInteractionTest.kt similarity index 90% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AnnouncementsListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AnnouncementsListInteractionTest.kt index 0c2ef77360..b5ab630e5b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AnnouncementsListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AnnouncementsListInteractionTest.kt @@ -14,27 +14,27 @@ * limitations under the License. * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import androidx.test.espresso.matcher.ViewMatchers.withId import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesCheckNames import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesViews -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Tab import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.`is` import org.junit.Test @HiltAndroidTest -class AnnouncementsListPageTest : TeacherTest() { +class AnnouncementsListInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssigneeListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssigneeListInteractionTest.kt similarity index 89% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssigneeListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssigneeListInteractionTest.kt index c1281f126b..e0dda5d529 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssigneeListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssigneeListInteractionTest.kt @@ -13,24 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.espresso.assertContainsText import com.instructure.espresso.page.onViewWithId import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class AssigneeListPageTest : TeacherComposeTest() { +class AssigneeListInteractionTest : TeacherComposeTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDetailsInteractionTest.kt similarity index 92% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDetailsInteractionTest.kt index b4a4135739..8978ddb7b7 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.init -import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvas.espresso.mockcanvas.utils.Randomizer import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Assignment.SubmissionType import com.instructure.canvasapi2.models.Assignment.SubmissionType.ONLINE_TEXT_ENTRY @@ -34,12 +34,12 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class AssignmentDetailsPageTest : TeacherComposeTest() { +class AssignmentDetailsInteractionTest : TeacherComposeTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDueDatesPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDueDatesInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDueDatesPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDueDatesInteractionTest.kt index f01494909a..aa844bc695 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDueDatesPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDueDatesInteractionTest.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.dataseeding.util.ago @@ -26,12 +26,12 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class AssignmentDueDatesPageTest : TeacherComposeTest() { +class AssignmentDueDatesInteractionTest : TeacherComposeTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentSubmissionListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentSubmissionListInteractionTest.kt similarity index 90% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentSubmissionListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentSubmissionListInteractionTest.kt index d739f4d407..a64bc47400 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentSubmissionListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentSubmissionListInteractionTest.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -29,7 +29,7 @@ import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -37,7 +37,7 @@ import org.junit.Test @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class AssignmentSubmissionListPageTest : TeacherComposeTest() { +class AssignmentSubmissionListInteractionTest : TeacherComposeTest() { @BindValue @JvmField diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CommentLibraryPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CommentLibraryInteractionTest.kt similarity index 92% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CommentLibraryPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CommentLibraryInteractionTest.kt index f5c7935b80..3034e3bd18 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CommentLibraryPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CommentLibraryInteractionTest.kt @@ -14,29 +14,29 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -51,7 +51,7 @@ import com.instructure.canvasapi2.managers.graphql.SubmissionGradeManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -59,7 +59,7 @@ import org.junit.Test @UninstallModules(GraphQlApiModule::class) @HiltAndroidTest -class CommentLibraryPageTest : TeacherComposeTest() { +class CommentLibraryInteractionTest : TeacherComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseBrowserPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseBrowserInteractionTest.kt similarity index 81% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseBrowserPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseBrowserInteractionTest.kt index 23fab0637c..ea1f719da8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseBrowserPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseBrowserInteractionTest.kt @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class CourseBrowserPageTest : TeacherTest() { +class CourseBrowserInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseSettingsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseSettingsInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseSettingsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseSettingsInteractionTest.kt index 293c165756..d653ace168 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseSettingsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseSettingsInteractionTest.kt @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.espresso.randomString import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class CourseSettingsPageTest : TeacherTest() { +class CourseSettingsInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DashboardPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DashboardInteractionTest.kt similarity index 90% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DashboardPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DashboardInteractionTest.kt index ed8c118f6c..c31627f374 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DashboardPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DashboardInteractionTest.kt @@ -15,17 +15,17 @@ * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class DashboardPageTest : TeacherTest() { +class DashboardInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DiscussionsListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DiscussionsListInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DiscussionsListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DiscussionsListInteractionTest.kt index 1f962bc1a0..f5e4c6d1d5 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DiscussionsListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DiscussionsListInteractionTest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Tab import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class DiscussionsListPageTest : TeacherTest() { +class DiscussionsListInteractionTest : TeacherTest() { lateinit var course: Course diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditAssignmentDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditAssignmentDetailsInteractionTest.kt similarity index 94% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditAssignmentDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditAssignmentDetailsInteractionTest.kt index 0db5a0bc92..e738f9fa8f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditAssignmentDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditAssignmentDetailsInteractionTest.kt @@ -14,14 +14,14 @@ * limitations under the License. * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.utils.NumberHelper @@ -29,13 +29,13 @@ import com.instructure.espresso.randomDouble import com.instructure.espresso.randomString import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test @HiltAndroidTest -class EditAssignmentDetailsPageTest : TeacherComposeTest() { +class EditAssignmentDetailsInteractionTest : TeacherComposeTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditDashboardPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditDashboardInteractionTest.kt similarity index 89% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditDashboardPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditDashboardInteractionTest.kt index f39d3e15aa..9f0ef1cee4 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditDashboardPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditDashboardInteractionTest.kt @@ -15,17 +15,17 @@ * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class EditDashboardPageTest : TeacherTest() { +class EditDashboardInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditQuizDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditQuizDetailsInteractionTest.kt similarity index 94% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditQuizDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditQuizDetailsInteractionTest.kt index 24014291c9..4b2b241031 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditQuizDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditQuizDetailsInteractionTest.kt @@ -14,26 +14,26 @@ * limitations under the License. * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Quiz import com.instructure.espresso.randomString import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test @HiltAndroidTest -class EditQuizDetailsPageTest : TeacherTest() { +class EditQuizDetailsInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditSyllabusPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditSyllabusInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditSyllabusPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditSyllabusInteractionTest.kt index 464acc9690..8803246ace 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditSyllabusPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditSyllabusInteractionTest.kt @@ -14,15 +14,15 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import android.os.Build -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addCourseSettings -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addCourseSettings +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.CourseSettings @@ -32,12 +32,12 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.ActivityHelper import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class EditSyllabusPageTest : TeacherTest() { +class EditSyllabusInteractionTest : TeacherTest() { override fun displaysPageObjects() = Unit diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InAppUpdatePageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/InAppUpdateInteractionTest.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InAppUpdatePageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/InAppUpdateInteractionTest.kt index dcf6a78b7e..7958f4fa22 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InAppUpdatePageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/InAppUpdateInteractionTest.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import android.app.NotificationManager import android.content.Context @@ -23,16 +23,15 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.di.UpdateModule import com.instructure.pandautils.update.UpdateManager import com.instructure.pandautils.update.UpdatePrefs import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ModuleListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/ModuleListInteractionTest.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ModuleListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/ModuleListInteractionTest.kt index 5bcdec9eab..c8096e1a16 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ModuleListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/ModuleListInteractionTest.kt @@ -16,15 +16,15 @@ * */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addItemToModule -import com.instructure.canvas.espresso.mockCanvas.addModuleToCourse -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addItemToModule +import com.instructure.canvas.espresso.mockcanvas.addModuleToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.ModuleContentDetails @@ -32,13 +32,13 @@ import com.instructure.canvasapi2.models.Tab import com.instructure.dataseeding.util.Randomizer import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.openOverflowMenu -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.openOverflowMenu +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class ModuleListPageTest : TeacherComposeTest() { +class ModuleListInteractionTest : TeacherComposeTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NavDrawerPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NavDrawerInteractionTest.kt similarity index 81% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NavDrawerPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NavDrawerInteractionTest.kt index 4e05ba7cd3..4003679480 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NavDrawerPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NavDrawerInteractionTest.kt @@ -15,19 +15,19 @@ * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.openLeftSideMenu -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.openLeftSideMenu +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class NavDrawerPageTest: TeacherTest() { +class NavDrawerInteractionTest: TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NotATeacherPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NotATeacherInteractionTest.kt similarity index 82% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NotATeacherPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NotATeacherInteractionTest.kt index fdabaaf522..60e867987a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NotATeacherPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NotATeacherInteractionTest.kt @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.slowLogInAsStudent +import com.instructure.teacher.ui.utils.extensions.slowLogInAsStudent import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class NotATeacherPageTest : TeacherTest() { +class NotATeacherInteractionTest : TeacherTest() { // Runs live; no MockCanvas @Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PageListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PageListInteractionTest.kt similarity index 86% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PageListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PageListInteractionTest.kt index db39f7c554..77cf1e453b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PageListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PageListInteractionTest.kt @@ -13,22 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addPageToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addPageToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Page import com.instructure.canvasapi2.models.Tab import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class PageListPageTest : TeacherTest() { +class PageListInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PersonContextPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PersonContextInteractionTest.kt similarity index 83% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PersonContextPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PersonContextInteractionTest.kt index 16c01111a8..c72f6c44ee 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PersonContextPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PersonContextInteractionTest.kt @@ -13,23 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionsForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionsForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -46,9 +46,9 @@ import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.models.User -import com.instructure.teacher.ui.pages.PersonContextPage +import com.instructure.teacher.ui.pages.classic.PersonContextPage import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -56,7 +56,7 @@ import org.junit.Test @UninstallModules(GraphQlApiModule::class) @HiltAndroidTest -class PersonContextPageTest : TeacherTest() { +class PersonContextInteractionTest : TeacherTest() { @BindValue @JvmField diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizDetailsInteractionTest.kt similarity index 89% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizDetailsInteractionTest.kt index d6177861e7..03b35f3204 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizDetailsInteractionTest.kt @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addQuizSubmission -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addQuizSubmission +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Quiz import com.instructure.dataseeding.util.ago @@ -27,12 +27,12 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class QuizDetailsPageTest: TeacherTest() { +class QuizDetailsInteractionTest: TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizListInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizListInteractionTest.kt index 2a0ad9c017..d3d2bacef4 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizListInteractionTest.kt @@ -14,24 +14,24 @@ * limitations under the License. * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Quiz import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class QuizListPageTest : TeacherTest() { +class QuizListInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizSubmissionListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizSubmissionListInteractionTest.kt similarity index 90% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizSubmissionListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizSubmissionListInteractionTest.kt index a05f0e8d8a..a747d99a1a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizSubmissionListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizSubmissionListInteractionTest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addQuestionToQuiz -import com.instructure.canvas.espresso.mockCanvas.addQuizSubmission -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addQuestionToQuiz +import com.instructure.canvas.espresso.mockcanvas.addQuizSubmission +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.CanvasContextPermission @@ -32,7 +32,7 @@ import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -40,7 +40,7 @@ import org.junit.Test @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class QuizSubmissionListPageTest : TeacherComposeTest() { +class QuizSubmissionListInteractionTest : TeacherComposeTest() { @BindValue @JvmField diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderCommentsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderCommentsInteractionTest.kt similarity index 93% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderCommentsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderCommentsInteractionTest.kt index e8dcf11b66..59cd3266a7 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderCommentsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderCommentsInteractionTest.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionsForAssignment -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionsForAssignment +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.CanvasContextPermission @@ -28,12 +28,12 @@ import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.SubmissionComment import com.instructure.espresso.randomString import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class SpeedGraderCommentsPageTest : TeacherComposeTest() { +class SpeedGraderCommentsInteractionTest : TeacherComposeTest() { // Just good enough to mock the *representation* of a file, not to mock the file itself. val attachment = Attachment( diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderFilesPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderFilesInteractionTest.kt similarity index 82% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderFilesPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderFilesInteractionTest.kt index 8100a1af12..8dfa1a53a3 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderFilesPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderFilesInteractionTest.kt @@ -13,25 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.CommentLibraryManager @@ -49,7 +49,7 @@ import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -60,7 +60,7 @@ import org.junit.Test CustomGradeStatusModule::class ) @HiltAndroidTest -class SpeedGraderFilesPageTest : TeacherComposeTest() { +class SpeedGraderFilesInteractionTest : TeacherComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderGradeInteractionTest.kt similarity index 91% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderGradeInteractionTest.kt index 1e237d8a55..ca6e5ecaf6 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderGradeInteractionTest.kt @@ -13,28 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import android.util.Log -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.StubMultiAPILevel -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addRubricToAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.annotations.StubMultiAPILevel +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addRubricToAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.CommentLibraryManager @@ -58,7 +58,7 @@ import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -69,7 +69,7 @@ import org.junit.Test GraphQlApiModule::class, CustomGradeStatusModule::class ) -class SpeedGraderGradePageTest : TeacherComposeTest() { +class SpeedGraderGradeInteractionTest : TeacherComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderInteractionTest.kt index ad514be7e4..6d4b95732b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderInteractionTest.kt @@ -13,24 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionsForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionsForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -50,7 +50,7 @@ import com.instructure.canvasapi2.models.Assignment.SubmissionType.ON_PAPER import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -58,7 +58,7 @@ import org.junit.Test @HiltAndroidTest @UninstallModules(GraphQlApiModule::class) -class SpeedGraderPageTest : TeacherComposeTest() { +class SpeedGraderInteractionTest : TeacherComposeTest() { @BindValue @JvmField diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderQuizSubmissionPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderQuizSubmissionInteractionTest.kt similarity index 85% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderQuizSubmissionPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderQuizSubmissionInteractionTest.kt index 158f2b9dae..bdb235bca0 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderQuizSubmissionPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderQuizSubmissionInteractionTest.kt @@ -13,25 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addQuestionToQuiz -import com.instructure.canvas.espresso.mockCanvas.addQuizSubmission -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addQuestionToQuiz +import com.instructure.canvas.espresso.mockcanvas.addQuizSubmission +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Quiz import com.instructure.canvasapi2.models.QuizAnswer import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class SpeedGraderQuizSubmissionPageTest : TeacherComposeTest() { +class SpeedGraderQuizSubmissionInteractionTest : TeacherComposeTest() { @Stub @Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SyllabusInteractionTest.kt similarity index 89% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SyllabusInteractionTest.kt index 4a4f8c2720..a47e95020f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SyllabusInteractionTest.kt @@ -14,14 +14,14 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addCourseSettings -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addCourseSettings +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.CourseSettings @@ -30,12 +30,12 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class SyllabusPageTest : TeacherComposeTest() { +class SyllabusInteractionTest : TeacherComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarInteractionTest.kt similarity index 83% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarInteractionTest.kt index 2fbcbe6a09..852df550e5 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarInteractionTest.kt @@ -13,25 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction 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.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.espresso.ModuleItemInteractions import com.instructure.teacher.BuildConfig import com.instructure.teacher.R import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.AssignmentDetailsPage -import com.instructure.teacher.ui.pages.DashboardPage -import com.instructure.teacher.ui.pages.DiscussionsDetailsPage +import com.instructure.teacher.ui.pages.classic.AssignmentDetailsPage +import com.instructure.teacher.ui.pages.classic.DashboardPage +import com.instructure.teacher.ui.pages.classic.DiscussionsDetailsPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest -class TeacherCalendarPageTest : CalendarInteractionTest() { +class TeacherCalendarInteractionTest : CalendarInteractionTest() { override val activityRule = TeacherActivityTestRule(LoginActivity::class.java) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarToDoDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarToDoDetailsInteractionTest.kt similarity index 84% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarToDoDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarToDoDetailsInteractionTest.kt index aa5dbded25..75b4d0c99b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarToDoDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarToDoDetailsInteractionTest.kt @@ -13,23 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import android.app.Activity 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.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.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest -class TeacherCalendarToDoDetailsPageTest : ToDoDetailsInteractionTest() { +class TeacherCalendarToDoDetailsInteractionTest : ToDoDetailsInteractionTest() { override val activityRule: InstructureActivityTestRule = TeacherActivityTestRule(LoginActivity::class.java) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateEventPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateEventInteractionTest.kt similarity index 86% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateEventPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateEventInteractionTest.kt index ab55465afe..d2905669eb 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateEventPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateEventInteractionTest.kt @@ -13,21 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction 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.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest -class TeacherCreateUpdateEventPageTest : CreateUpdateEventInteractionTest() { +class TeacherCreateUpdateEventInteractionTest : CreateUpdateEventInteractionTest() { override val activityRule = TeacherActivityTestRule(LoginActivity::class.java) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateToDoInteractionTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateToDoInteractionTest.kt similarity index 89% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateToDoInteractionTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateToDoInteractionTest.kt index 8558ac33cb..2d22560b3f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateToDoInteractionTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateToDoInteractionTest.kt @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction 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.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherEventDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherEventDetailsInteractionTest.kt similarity index 82% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherEventDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherEventDetailsInteractionTest.kt index 4eb2e46cbf..7d8901a932 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherEventDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherEventDetailsInteractionTest.kt @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction 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.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest -class TeacherEventDetailsPageTest : EventDetailsInteractionTest() { +class TeacherEventDetailsInteractionTest : EventDetailsInteractionTest() { override val activityRule = TeacherActivityTestRule(LoginActivity::class.java) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxComposeInteractionTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxComposeInteractionTest.kt similarity index 85% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxComposeInteractionTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxComposeInteractionTest.kt index a9376f1b81..46a06545e0 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxComposeInteractionTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxComposeInteractionTest.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import androidx.compose.ui.platform.ComposeView import androidx.test.espresso.matcher.ViewMatchers @@ -22,20 +22,20 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.InboxComposeInteractionTest import com.instructure.canvas.espresso.common.pages.InboxPage -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.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +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.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -53,9 +53,9 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.User import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxListInteractionTest.kt similarity index 80% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxListInteractionTest.kt index bd709973b1..5cc8dbd17d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxListInteractionTest.kt @@ -13,25 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction 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.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.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.clickInboxTab -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.clickInboxTab +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest -class TeacherInboxListPageTest : InboxListInteractionTest() { +class TeacherInboxListInteractionTest : InboxListInteractionTest() { override val isTesting = BuildConfig.IS_TESTING override val activityRule = TeacherActivityTestRule(LoginActivity::class.java) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxSignatureInteractionTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxSignatureInteractionTest.kt similarity index 79% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxSignatureInteractionTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxSignatureInteractionTest.kt index d96cd99063..bba43e659b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxSignatureInteractionTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxSignatureInteractionTest.kt @@ -13,21 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import com.instructure.canvas.espresso.common.interaction.InboxSignatureInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -41,11 +41,11 @@ import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.managers.graphql.SubmissionGradeManager import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage -import com.instructure.teacher.ui.pages.LeftSideNavigationDrawerPage +import com.instructure.teacher.ui.pages.classic.DashboardPage +import com.instructure.teacher.ui.pages.classic.LeftSideNavigationDrawerPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.openLeftSideMenu -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.openLeftSideMenu +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/UpdateFilePermissionsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/UpdateFilePermissionsInteractionTest.kt similarity index 92% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/UpdateFilePermissionsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/UpdateFilePermissionsInteractionTest.kt index 3fb6f84c2e..e5f4904e46 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/UpdateFilePermissionsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/UpdateFilePermissionsInteractionTest.kt @@ -16,28 +16,28 @@ * */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addItemToModule -import com.instructure.canvas.espresso.mockCanvas.addModuleToCourse -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addItemToModule +import com.instructure.canvas.espresso.mockcanvas.addModuleToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.ModuleContentDetails import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.util.Randomizer import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.util.Calendar import java.util.Date @HiltAndroidTest -class UpdateFilePermissionsPageTest : TeacherTest() { +class UpdateFilePermissionsInteractionTest : TeacherTest() { override fun displaysPageObjects() = Unit diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AllCoursesListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AllCoursesListPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AllCoursesListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AllCoursesListPage.kt index 8c5ee233cc..c928ce451d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AllCoursesListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AllCoursesListPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.view.View import com.instructure.canvasapi2.models.Course diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AnnouncementsListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AnnouncementsListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AnnouncementsListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AnnouncementsListPage.kt index 932b13e31f..5882237770 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AnnouncementsListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AnnouncementsListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.assertion.ViewAssertions diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssigneeListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssigneeListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssigneeListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssigneeListPage.kt index 51faf5e03d..e8f754dbdd 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssigneeListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssigneeListPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.recyclerview.widget.RecyclerView diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDetailsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDetailsPage.kt index 62cafea34c..fd9a5cdabc 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDetailsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.annotation.StringRes diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDueDatesPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDueDatesPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDueDatesPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDueDatesPage.kt index 6390d9135a..360397797b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDueDatesPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDueDatesPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.espresso.OnViewWithContentDescription diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CommentLibraryPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CommentLibraryPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CommentLibraryPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CommentLibraryPage.kt index 1f1d2183a0..78bd8f09c4 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CommentLibraryPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CommentLibraryPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.espresso.OnViewWithId import com.instructure.espresso.RecyclerViewItemCountAssertion diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseBrowserPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseBrowserPage.kt index 038f29690f..a8311a954a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseBrowserPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.NoMatchingViewException diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseSettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseSettingsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseSettingsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseSettingsPage.kt index b01492d45b..4cfe0ff5cc 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseSettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseSettingsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DashboardPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DashboardPage.kt index 85b2d89c00..f2fb327d64 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DashboardPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.view.View import androidx.test.espresso.assertion.ViewAssertions.doesNotExist diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsDetailsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsDetailsPage.kt index 155564113d..3cfebae854 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.assertion.WebViewAssertions diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsListPage.kt index dee91ab3f9..8bcf42c3b9 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAnnouncementDetailsPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAnnouncementDetailsPage.kt index 9415744a68..a3c704e58d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAnnouncementDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import com.instructure.espresso.click diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAssignmentDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAssignmentDetailsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAssignmentDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAssignmentDetailsPage.kt index e58ed863aa..686284c2b3 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAssignmentDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAssignmentDetailsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.widget.DatePicker diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDashboardPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDashboardPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDashboardPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDashboardPage.kt index aaee4926e4..5aa62ddac3 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDashboardPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDashboardPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.assertion.ViewAssertions.matches diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDiscussionsDetailsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDiscussionsDetailsPage.kt index e89ad6cc21..8d946573db 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDiscussionsDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import com.instructure.espresso.WaitForViewWithId diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditPageDetailsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditPageDetailsPage.kt index 6f89f22fd8..126611630d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditPageDetailsPage.kt @@ -1,4 +1,4 @@ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditProfileSettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditProfileSettingsPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditProfileSettingsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditProfileSettingsPage.kt index 2176737592..4f5c9f008b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditProfileSettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditProfileSettingsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.espresso.OnViewWithId import com.instructure.espresso.clearText diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditQuizDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditQuizDetailsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditQuizDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditQuizDetailsPage.kt index 5f575a0846..9db7b2eb49 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditQuizDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditQuizDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.widget.DatePicker diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditSyllabusPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditSyllabusPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditSyllabusPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditSyllabusPage.kt index 0723ba0e80..67203f5c21 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditSyllabusPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditSyllabusPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/FileListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/FileListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/FileListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/FileListPage.kt index 3cd71b51e4..b343143c9f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/FileListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/FileListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.appcompat.widget.AppCompatButton import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/HelpPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/HelpPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/HelpPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/HelpPage.kt index 005ff36fb8..2609b28e5f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/HelpPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/HelpPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.app.Instrumentation import android.content.Intent diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/LeftSideNavigationDrawerPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/LeftSideNavigationDrawerPage.kt index 89bcbc6e8e..6c0d83251a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/LeftSideNavigationDrawerPage.kt @@ -1,4 +1,4 @@ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.view.View import androidx.appcompat.widget.SwitchCompat diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ModulesPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ModulesPage.kt index 8b1cd6c598..77abf532fd 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ModulesPage.kt @@ -1,4 +1,4 @@ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.annotation.StringRes import androidx.test.espresso.matcher.ViewMatchers.hasSibling diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NavDrawerPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NavDrawerPage.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NavDrawerPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NavDrawerPage.kt index f4159fe6c8..84c89ea0aa 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NavDrawerPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NavDrawerPage.kt @@ -1,4 +1,4 @@ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withText diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NotATeacherPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NotATeacherPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NotATeacherPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NotATeacherPage.kt index d1ce0395c7..feb972d826 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NotATeacherPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NotATeacherPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.click diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PageListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PageListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PageListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PageListPage.kt index a42d4c9639..e9578656ae 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PageListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PageListPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.view.View import androidx.test.espresso.Espresso diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PeopleListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PeopleListPage.kt index bdb4cef085..53b312827d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PeopleListPage.kt @@ -12,7 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ package com.instructure.teacher.ui.pages + */ package com.instructure.teacher.ui.pages.classic import android.view.View import androidx.recyclerview.widget.RecyclerView diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PersonContextPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PersonContextPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PersonContextPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PersonContextPage.kt index 5c6775ad4c..bd4a8c3dbb 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PersonContextPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PersonContextPage.kt @@ -15,7 +15,7 @@ */ @file:Suppress("unused") -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.matches import com.instructure.canvas.espresso.containsTextCaseInsensitive diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PostSettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PostSettingsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PostSettingsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PostSettingsPage.kt index 57f4030342..c12de307a8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PostSettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PostSettingsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.espresso.WaitForViewWithId diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProfileSettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ProfileSettingsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProfileSettingsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ProfileSettingsPage.kt index 5c9e51b82b..be10fdb00b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProfileSettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ProfileSettingsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.matches import com.instructure.espresso.OnViewWithId diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PushNotificationsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PushNotificationsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PushNotificationsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PushNotificationsPage.kt index 612a39f914..1e81412e11 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PushNotificationsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PushNotificationsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizDetailsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizDetailsPage.kt index 7f683cdb4c..0deec368ca 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizDetailsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.InstrumentationRegistry import com.instructure.canvasapi2.models.Quiz diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizListPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizListPage.kt index 5898120932..faacf6f206 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.canvasapi2.models.Quiz import com.instructure.espresso.DoesNotExistAssertion diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/RemoteConfigSettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/RemoteConfigSettingsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/RemoteConfigSettingsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/RemoteConfigSettingsPage.kt index 284274d18e..c94595441f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/RemoteConfigSettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/RemoteConfigSettingsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.view.View import android.widget.EditText diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderCommentsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderCommentsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderCommentsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderCommentsPage.kt index 4948351dad..4dba8ed42a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderCommentsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderCommentsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers.Visibility diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderQuizSubmissionPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderQuizSubmissionPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderQuizSubmissionPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderQuizSubmissionPage.kt index 640dad24e1..00983dfdf3 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderQuizSubmissionPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderQuizSubmissionPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.webdriver.DriverAtoms diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/StudentContextPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/StudentContextPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/StudentContextPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/StudentContextPage.kt index 706ba1165f..2809cf3af9 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/StudentContextPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/StudentContextPage.kt @@ -15,9 +15,8 @@ */ @file:Suppress("unused") -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic -import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.assertHasText diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SyllabusPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SyllabusPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SyllabusPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SyllabusPage.kt index df93e38cf4..5351c68628 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SyllabusPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SyllabusPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.app.Activity import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/TodoPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/TodoPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/TodoPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/TodoPage.kt index e27e701eef..59c7ae8d3c 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/TodoPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/TodoPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.hasSibling import com.instructure.espresso.RecyclerViewItemCountAssertion diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/UpdateFilePermissionsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/UpdateFilePermissionsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/UpdateFilePermissionsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/UpdateFilePermissionsPage.kt index 4527b07dd1..da7a148e8d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/UpdateFilePermissionsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/UpdateFilePermissionsPage.kt @@ -16,7 +16,7 @@ * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.widget.DatePicker import android.widget.TimePicker diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/WebViewLoginPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/WebViewLoginPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/WebViewLoginPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/WebViewLoginPage.kt index 8559af1316..aa40a009e8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/WebViewLoginPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/WebViewLoginPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.sugar.Web.onWebView diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentSubmissionListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/AssignmentSubmissionListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentSubmissionListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/AssignmentSubmissionListPage.kt index bc22c3523f..5fcdc8f54d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentSubmissionListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/AssignmentSubmissionListPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.compose import androidx.compose.ui.test.assertCountEquals diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProgressPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/ProgressPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProgressPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/ProgressPage.kt index c357b1157d..3d419dcfd6 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProgressPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/ProgressPage.kt @@ -16,7 +16,7 @@ * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.compose import androidx.annotation.StringRes import androidx.compose.ui.test.ExperimentalTestApi diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderGradePage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderGradePage.kt index 9cf768c1d2..78ab0a180d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderGradePage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderGradePage.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextReplacement import androidx.test.espresso.Espresso -import com.instructure.composeTest.hasTestTagThatContains +import com.instructure.composetest.hasTestTagThatContains import com.instructure.espresso.OnViewWithId import com.instructure.espresso.OnViewWithText import com.instructure.espresso.WaitForViewWithId diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt index 4f267665dd..45e402a879 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt @@ -39,7 +39,7 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.User -import com.instructure.composeTest.hasTestTagThatContains +import com.instructure.composetest.hasTestTagThatContains import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.SubmissionApiModel import com.instructure.espresso.OnViewWithId diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/EditSyllabusRenderTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/EditSyllabusRenderTest.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/EditSyllabusRenderTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/EditSyllabusRenderTest.kt index df7fc5f00d..2d88bc6a21 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/EditSyllabusRenderTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/EditSyllabusRenderTest.kt @@ -14,12 +14,12 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.renderTests +package com.instructure.teacher.ui.rendertests import com.instructure.canvasapi2.models.Course import com.instructure.teacher.features.syllabus.edit.EditSyllabusFragment import com.instructure.teacher.features.syllabus.edit.EditSyllabusViewState -import com.instructure.teacher.ui.renderTests.pages.EditSyllabusRenderPage +import com.instructure.teacher.ui.rendertests.renderpages.EditSyllabusRenderPage import com.instructure.teacher.ui.utils.TeacherRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/ModuleListRenderTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/ModuleListRenderTest.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/ModuleListRenderTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/ModuleListRenderTest.kt index 31bf453bbf..e8c3910048 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/ModuleListRenderTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/ModuleListRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.renderTests +package com.instructure.teacher.ui.rendertests import android.graphics.Color import androidx.test.espresso.assertion.ViewAssertions.matches @@ -32,7 +32,7 @@ import com.instructure.teacher.R import com.instructure.teacher.features.modules.list.ui.ModuleListFragment import com.instructure.teacher.features.modules.list.ui.ModuleListItemData import com.instructure.teacher.features.modules.list.ui.ModuleListViewState -import com.instructure.teacher.ui.renderTests.pages.ModuleListRenderPage +import com.instructure.teacher.ui.rendertests.renderpages.ModuleListRenderPage import com.instructure.teacher.ui.utils.TeacherRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/PostGradeRenderTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/PostGradeRenderTest.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/PostGradeRenderTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/PostGradeRenderTest.kt index 4265baf5f8..9074d3a1db 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/PostGradeRenderTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/PostGradeRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.renderTests +package com.instructure.teacher.ui.rendertests import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Section @@ -22,7 +22,7 @@ import com.instructure.teacher.R import com.instructure.teacher.features.postpolicies.PostSection import com.instructure.teacher.features.postpolicies.ui.PostGradeFragment import com.instructure.teacher.features.postpolicies.ui.PostGradeViewState -import com.instructure.teacher.ui.renderTests.pages.PostGradeRenderPage +import com.instructure.teacher.ui.rendertests.renderpages.PostGradeRenderPage import com.instructure.teacher.ui.utils.TeacherRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/SyllabusRenderTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/SyllabusRenderTest.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/SyllabusRenderTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/SyllabusRenderTest.kt index 448872748b..253fb56a5b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/SyllabusRenderTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/SyllabusRenderTest.kt @@ -14,16 +14,16 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.renderTests +package com.instructure.teacher.ui.rendertests -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.teacher.features.syllabus.SyllabusModel import com.instructure.teacher.features.syllabus.ui.SyllabusFragment -import com.instructure.teacher.ui.renderTests.pages.SyllabusRenderPage +import com.instructure.teacher.ui.rendertests.renderpages.SyllabusRenderPage import com.instructure.teacher.ui.utils.TeacherRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/EditSyllabusRenderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/EditSyllabusRenderPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/EditSyllabusRenderPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/EditSyllabusRenderPage.kt index 1936839aaa..88f64cc57a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/EditSyllabusRenderPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/EditSyllabusRenderPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.renderTests.pages +package com.instructure.teacher.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/ModuleListRenderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/ModuleListRenderPage.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/ModuleListRenderPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/ModuleListRenderPage.kt index 7fa38d027c..d448abd6f3 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/ModuleListRenderPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/ModuleListRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.renderTests.pages +package com.instructure.teacher.ui.rendertests.renderpages import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView @@ -21,6 +21,8 @@ import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import com.instructure.canvas.espresso.SwipeRefreshLayoutMatchers +import com.instructure.canvas.espresso.ViewSizeMatcher import com.instructure.espresso.OnViewWithId import com.instructure.espresso.RecyclerViewItemCountAssertion import com.instructure.espresso.assertDisplayed @@ -29,8 +31,6 @@ import com.instructure.espresso.page.onView import com.instructure.espresso.page.withAncestor import com.instructure.espresso.page.withText import com.instructure.teacher.R -import com.instructure.teacher.ui.utils.SwipeRefreshLayoutMatchers -import com.instructure.teacher.ui.utils.ViewSizeMatcher import org.hamcrest.CoreMatchers.allOf class ModuleListRenderPage : BasePage(R.id.moduleList) { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/PostGradeRenderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/PostGradeRenderPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/PostGradeRenderPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/PostGradeRenderPage.kt index 79394d11fd..3096516a58 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/PostGradeRenderPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/PostGradeRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.renderTests.pages +package com.instructure.teacher.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/SyllabusRenderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/SyllabusRenderPage.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/SyllabusRenderPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/SyllabusRenderPage.kt index 73d20f7ebc..3a21a3d01d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/SyllabusRenderPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/SyllabusRenderPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.renderTests.pages +package com.instructure.teacher.ui.rendertests.renderpages import androidx.test.espresso.action.ViewActions import com.instructure.espresso.OnViewWithId @@ -28,7 +28,7 @@ import com.instructure.espresso.page.withId import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.teacher.R -import com.instructure.teacher.ui.pages.SyllabusPage +import com.instructure.teacher.ui.pages.classic.SyllabusPage import org.hamcrest.CoreMatchers import org.hamcrest.Matchers diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/Matchers.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/Matchers.kt deleted file mode 100644 index eb5c6ac8bc..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/Matchers.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2019 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.instructure.teacher.ui.utils - -import android.view.View -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import androidx.test.espresso.matcher.BoundedMatcher -import org.hamcrest.Description -import org.hamcrest.Matcher -import org.hamcrest.TypeSafeMatcher - -object SwipeRefreshLayoutMatchers { - fun isRefreshing(isRefreshing: Boolean): Matcher { - return object : BoundedMatcher(SwipeRefreshLayout::class.java) { - - override fun describeTo(description: Description) { - description.appendText(if (isRefreshing) "is refreshing" else "is not refreshing") - } - - override fun matchesSafely(view: SwipeRefreshLayout): Boolean { - return view.isRefreshing == isRefreshing - } - } - } -} - -object ViewSizeMatcher { - fun hasWidth(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { - override fun describeTo(description: Description) { - description.appendText("has a width of ${pixels}px") - } - - override fun matchesSafely(view: View): Boolean = view.width == pixels - } - - fun hasHeight(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { - override fun describeTo(description: Description) { - description.appendText("has a height of ${pixels}px") - } - - override fun matchesSafely(view: View): Boolean = view.height == pixels - } - - fun hasMinWidth(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { - override fun describeTo(description: Description) { - description.appendText("has a minimum width of ${pixels}px") - } - - override fun matchesSafely(view: View): Boolean = view.width >= pixels - } - - fun hasMinHeight(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { - override fun describeTo(description: Description) { - description.appendText("has a minimum height of ${pixels}px") - } - - override fun matchesSafely(view: View): Boolean = view.height >= pixels - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt index 3811148952..ffa88fdb1e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt @@ -33,8 +33,8 @@ import com.instructure.canvas.espresso.common.pages.compose.RecipientPickerPage import com.instructure.canvas.espresso.common.pages.compose.SelectContextPage import com.instructure.canvas.espresso.common.pages.compose.SettingsPage import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.AssignmentSubmissionListPage -import com.instructure.teacher.ui.pages.ProgressPage +import com.instructure.teacher.ui.pages.compose.AssignmentSubmissionListPage +import com.instructure.teacher.ui.pages.compose.ProgressPage import com.instructure.teacher.ui.pages.compose.SpeedGraderGradePage import com.instructure.teacher.ui.pages.compose.SpeedGraderPage diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt index 4267822a86..676497f1ed 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt @@ -38,47 +38,47 @@ import com.instructure.espresso.Searchable import com.instructure.teacher.BuildConfig import com.instructure.teacher.R import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.espresso.TestAppManager -import com.instructure.teacher.ui.pages.AnnouncementsListPage -import com.instructure.teacher.ui.pages.AssigneeListPage -import com.instructure.teacher.ui.pages.AssignmentDetailsPage -import com.instructure.teacher.ui.pages.AssignmentDueDatesPage -import com.instructure.teacher.ui.pages.CommentLibraryPage -import com.instructure.teacher.ui.pages.CourseBrowserPage -import com.instructure.teacher.ui.pages.CourseSettingsPage -import com.instructure.teacher.ui.pages.DashboardPage -import com.instructure.teacher.ui.pages.DiscussionsDetailsPage -import com.instructure.teacher.ui.pages.DiscussionsListPage -import com.instructure.teacher.ui.pages.EditAnnouncementDetailsPage -import com.instructure.teacher.ui.pages.EditAssignmentDetailsPage -import com.instructure.teacher.ui.pages.EditDashboardPage -import com.instructure.teacher.ui.pages.EditDiscussionsDetailsPage -import com.instructure.teacher.ui.pages.EditPageDetailsPage -import com.instructure.teacher.ui.pages.EditProfileSettingsPage -import com.instructure.teacher.ui.pages.EditQuizDetailsPage -import com.instructure.teacher.ui.pages.EditSyllabusPage -import com.instructure.teacher.ui.pages.FileListPage -import com.instructure.teacher.ui.pages.HelpPage -import com.instructure.teacher.ui.pages.LeftSideNavigationDrawerPage -import com.instructure.teacher.ui.pages.ModulesPage -import com.instructure.teacher.ui.pages.NavDrawerPage -import com.instructure.teacher.ui.pages.NotATeacherPage -import com.instructure.teacher.ui.pages.PageListPage -import com.instructure.teacher.ui.pages.PeopleListPage -import com.instructure.teacher.ui.pages.PersonContextPage -import com.instructure.teacher.ui.pages.PostSettingsPage -import com.instructure.teacher.ui.pages.ProfileSettingsPage -import com.instructure.teacher.ui.pages.PushNotificationsPage -import com.instructure.teacher.ui.pages.QuizDetailsPage -import com.instructure.teacher.ui.pages.QuizListPage -import com.instructure.teacher.ui.pages.RemoteConfigSettingsPage -import com.instructure.teacher.ui.pages.SpeedGraderCommentsPage -import com.instructure.teacher.ui.pages.SpeedGraderQuizSubmissionPage -import com.instructure.teacher.ui.pages.StudentContextPage -import com.instructure.teacher.ui.pages.SyllabusPage -import com.instructure.teacher.ui.pages.TodoPage -import com.instructure.teacher.ui.pages.UpdateFilePermissionsPage -import com.instructure.teacher.ui.pages.WebViewLoginPage +import com.instructure.teacher.espresso.TestAppManager +import com.instructure.teacher.ui.pages.classic.AnnouncementsListPage +import com.instructure.teacher.ui.pages.classic.AssigneeListPage +import com.instructure.teacher.ui.pages.classic.AssignmentDetailsPage +import com.instructure.teacher.ui.pages.classic.AssignmentDueDatesPage +import com.instructure.teacher.ui.pages.classic.CommentLibraryPage +import com.instructure.teacher.ui.pages.classic.CourseBrowserPage +import com.instructure.teacher.ui.pages.classic.CourseSettingsPage +import com.instructure.teacher.ui.pages.classic.DashboardPage +import com.instructure.teacher.ui.pages.classic.DiscussionsDetailsPage +import com.instructure.teacher.ui.pages.classic.DiscussionsListPage +import com.instructure.teacher.ui.pages.classic.EditAnnouncementDetailsPage +import com.instructure.teacher.ui.pages.classic.EditAssignmentDetailsPage +import com.instructure.teacher.ui.pages.classic.EditDashboardPage +import com.instructure.teacher.ui.pages.classic.EditDiscussionsDetailsPage +import com.instructure.teacher.ui.pages.classic.EditPageDetailsPage +import com.instructure.teacher.ui.pages.classic.EditProfileSettingsPage +import com.instructure.teacher.ui.pages.classic.EditQuizDetailsPage +import com.instructure.teacher.ui.pages.classic.EditSyllabusPage +import com.instructure.teacher.ui.pages.classic.FileListPage +import com.instructure.teacher.ui.pages.classic.HelpPage +import com.instructure.teacher.ui.pages.classic.LeftSideNavigationDrawerPage +import com.instructure.teacher.ui.pages.classic.ModulesPage +import com.instructure.teacher.ui.pages.classic.NavDrawerPage +import com.instructure.teacher.ui.pages.classic.NotATeacherPage +import com.instructure.teacher.ui.pages.classic.PageListPage +import com.instructure.teacher.ui.pages.classic.PeopleListPage +import com.instructure.teacher.ui.pages.classic.PersonContextPage +import com.instructure.teacher.ui.pages.classic.PostSettingsPage +import com.instructure.teacher.ui.pages.classic.ProfileSettingsPage +import com.instructure.teacher.ui.pages.classic.PushNotificationsPage +import com.instructure.teacher.ui.pages.classic.QuizDetailsPage +import com.instructure.teacher.ui.pages.classic.QuizListPage +import com.instructure.teacher.ui.pages.classic.RemoteConfigSettingsPage +import com.instructure.teacher.ui.pages.classic.SpeedGraderCommentsPage +import com.instructure.teacher.ui.pages.classic.SpeedGraderQuizSubmissionPage +import com.instructure.teacher.ui.pages.classic.StudentContextPage +import com.instructure.teacher.ui.pages.classic.SyllabusPage +import com.instructure.teacher.ui.pages.classic.TodoPage +import com.instructure.teacher.ui.pages.classic.UpdateFilePermissionsPage +import com.instructure.teacher.ui.pages.classic.WebViewLoginPage import instructure.rceditor.RCETextEditor import org.hamcrest.Matcher import org.junit.Before diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/ViewInteractionDelegates.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/ViewInteractionDelegates.kt deleted file mode 100644 index 31b9c2a367..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/ViewInteractionDelegates.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2018 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.instructure.teacher.ui.utils - -import androidx.test.espresso.Espresso -import androidx.test.espresso.ViewInteraction -import androidx.test.espresso.matcher.ViewMatchers -import android.view.View -import com.instructure.espresso.ViewInteractionDelegate -import com.instructure.teacher.R -import org.hamcrest.Matcher -import org.hamcrest.Matchers - -/** - * The toolbar's title text view's resource id is the same as the course text view in course cards. - * Use this to narrow the matcher to the toolbar itself. - */ -class WaitForToolbarTitle(val text: Int, autoAssert: Boolean = true) : ViewInteractionDelegate(autoAssert) { - override fun onProvide(matcher: Matcher): ViewInteraction = Espresso.onView(matcher) - override fun getMatcher(): Matcher { - return Matchers.allOf(ViewMatchers.withText(text), ViewMatchers.withParent(ViewMatchers.withId(R.id.toolbar))) - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/PageExtensions.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/PageInteractionExtensions.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/PageExtensions.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/PageInteractionExtensions.kt index d18f5e620e..1e787286dd 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/PageExtensions.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/PageInteractionExtensions.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.utils +package com.instructure.teacher.ui.utils.extensions import androidx.test.espresso.action.ViewActions import com.instructure.espresso.click diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/TeacherTestExtensions.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/TeacherTestExtensions.kt index 4c9e721f98..1e4de4c00e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/TeacherTestExtensions.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.teacher.ui.utils +package com.instructure.teacher.ui.utils.extensions import android.app.Activity import android.content.Intent @@ -60,6 +60,7 @@ import com.instructure.interactions.router.Route import com.instructure.teacher.R import com.instructure.teacher.activities.LoginActivity import com.instructure.teacher.router.RouteMatcher +import com.instructure.teacher.ui.utils.TeacherTest import org.hamcrest.CoreMatchers.allOf import org.hamcrest.Matchers.anyOf import java.io.BufferedInputStream diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/WebInteractionExtensions.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/WebInteractionExtensions.kt similarity index 71% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/WebInteractionExtensions.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/WebInteractionExtensions.kt index f855b406d8..cf334ae429 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/WebInteractionExtensions.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/WebInteractionExtensions.kt @@ -1,4 +1,20 @@ -package com.instructure.teacher.ui.utils +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.teacher.ui.utils.extensions import androidx.test.espresso.web.model.Atom import androidx.test.espresso.web.model.Evaluation diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasRunner.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasRunner.kt index af88cb5b23..beb2a8e6c2 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasRunner.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasRunner.kt @@ -8,7 +8,7 @@ import android.view.accessibility.AccessibilityNodeInfo import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingResource import androidx.test.runner.AndroidJUnitRunner -import com.instructure.canvas.espresso.mockCanvas.MockCanvasInterceptor +import com.instructure.canvas.espresso.mockcanvas.MockCanvasInterceptor import com.instructure.canvasapi2.CanvasRestAdapter import com.jakewharton.espresso.OkHttp3IdlingResource diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt index 6655d1f772..d5d98c264a 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt @@ -143,7 +143,7 @@ abstract class CanvasTest : InstructureTestingContract { // Only continue if we're on Bitrise // (More accurately, if we are on FTL launched from Bitrise.) - if(splunkToken != null && !splunkToken.isEmpty()) { + if(!splunkToken.isNullOrEmpty()) { val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) val hasActiveNetwork = networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false @@ -272,7 +272,7 @@ abstract class CanvasTest : InstructureTestingContract { // Suppression (exclusion) rules ?.setSuppressingResultMatcher( anyOf( - // Extra supressions that can be adde to individual tests + // Extra suppressions that can be added to individual tests extraAccessibilitySupressions, // Suppress issues in PsPDFKit, date/time picker, calendar grid isExcludedNamedView( listOf("pspdf", "_picker_", "timePicker", "calendar1")), @@ -346,18 +346,18 @@ abstract class CanvasTest : InstructureTestingContract { // Weeding out id == -1 will filter out a lot of lines from our logs if(view.id != -1) { try { - var resourceName = view.context.resources.getResourceName(view.id) + val resourceName = view.context.resources.getResourceName(view.id) for (excludedName in excludes) { if (resourceName.contains(excludedName)) { //Log.v("AccessibilityExclude", "Caught $resourceName") return true } } - } catch (e: Resources.NotFoundException) { + } catch (_: Resources.NotFoundException) { } } - var parent = view.parent + val parent = view.parent when(parent) { is View -> view = parent else -> view = null @@ -393,7 +393,7 @@ abstract class CanvasTest : InstructureTestingContract { // in action bars being to narrow. fun withOnlyWidthLessThan(dimInDp: Int) : BaseMatcher { - var activity = activityRule.activity + val activity = activityRule.activity val densityDpi = activity.resources.displayMetrics.densityDpi val dim_f = dimInDp * (densityDpi.toDouble() / DisplayMetrics.DENSITY_DEFAULT.toDouble()) val dim = dim_f.toInt() @@ -432,7 +432,7 @@ abstract class CanvasTest : InstructureTestingContract { when(item) { is AccessibilityViewCheckResult -> { val contentDescription = item.view?.contentDescription - var result = (contentDescription?.contains("Overflow", ignoreCase = true) ?: false) || (contentDescription?.contains("More options", ignoreCase = true) ?: false) + val result = (contentDescription?.contains("Overflow", ignoreCase = true) ?: false) || (contentDescription?.contains("More options", ignoreCase = true) ?: false) //Log.v("overflowWidth", "isOverflowMenu: contentDescription=${item.view?.contentDescription ?: "unknown"}, result=$result ") return result } @@ -474,7 +474,7 @@ abstract class CanvasTest : InstructureTestingContract { (v.width < 48 && v.width < v.minimumWidth) ) if(toss) { - var resourceName = getResourceName(v) + val resourceName = getResourceName(v) Log.v("underMinSizeOnLowRes", "Tossing $resourceName, w=${v.width}, h=${v.height}, mw=${v.minimumWidth}, mh=${v.minimumHeight}") return true @@ -517,7 +517,7 @@ abstract class CanvasTest : InstructureTestingContract { inputStream.copyTo(outputStream) } finally { - if(inputStream != null) inputStream.close() + inputStream?.close() if(outputStream != null) { outputStream.flush() outputStream.close() diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomActions.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomActions.kt index fb87884d51..f6dcedf239 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomActions.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomActions.kt @@ -26,6 +26,7 @@ import android.widget.EditText import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.PerformException @@ -37,6 +38,7 @@ import androidx.test.espresso.action.GeneralClickAction import androidx.test.espresso.action.Press import androidx.test.espresso.action.Tap import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.RootMatchers.withDecorView @@ -354,4 +356,29 @@ fun checkToastText(@StringRes stringRes: Int, activity: Activity) { //Intentionally empty as we would like to wait for the toast to disappear. Somehow doesNotExist() doesn't work because it passes even if the toast is still there and visible. } } +} + +fun pressBackButton(times: Int) { + for(i in 1..times) { + Espresso.pressBack() + } +} + +fun waitForViewToDisappear(viewMatcher: Matcher, timeoutInSeconds: Long) { + val startTime = System.currentTimeMillis() + + while (System.currentTimeMillis() - startTime < (timeoutInSeconds * 1000)) { + try { + onView(viewMatcher) + .check(ViewAssertions.doesNotExist()) + return + } catch (e: AssertionError) { + Thread.sleep(200) + } + } + throw AssertionError("The view has not been displayed within $timeoutInSeconds seconds.") +} + +fun toString(view: View): String { + return HumanReadables.getViewHierarchyErrorMessage(view, null, "", null) } \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt index e8565f704f..f309b7b826 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt @@ -16,16 +16,23 @@ */ package com.instructure.canvas.espresso +import android.content.Intent +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.Drawable import android.util.DisplayMetrics import android.view.View import android.view.ViewGroup import android.widget.EditText +import android.widget.ImageView import android.widget.RadioButton import android.widget.TextView import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.Toolbar import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.test.espresso.Espresso.onView import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction @@ -33,6 +40,7 @@ import androidx.test.espresso.ViewAssertion import androidx.test.espresso.ViewInteraction import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.BoundedMatcher +import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId @@ -42,8 +50,10 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.textfield.TextInputLayout import com.instructure.espresso.ActivityHelper +import com.instructure.pandautils.utils.ColorUtils import junit.framework.AssertionFailedError import org.hamcrest.BaseMatcher +import org.hamcrest.CoreMatchers.`is` import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.TypeSafeMatcher @@ -352,3 +362,189 @@ fun getText(matcher: Matcher): String { return text } +object SwipeRefreshLayoutMatchers { + fun isRefreshing(isRefreshing: Boolean): Matcher { + return object : BoundedMatcher(SwipeRefreshLayout::class.java) { + + override fun describeTo(description: Description) { + description.appendText(if (isRefreshing) "is refreshing" else "is not refreshing") + } + + override fun matchesSafely(view: SwipeRefreshLayout): Boolean { + return view.isRefreshing == isRefreshing + } + } + } +} + +object ViewSizeMatcher { + fun hasWidth(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("has a width of ${pixels}px") + } + + override fun matchesSafely(view: View): Boolean = view.width == pixels + } + + fun hasHeight(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("has a height of ${pixels}px") + } + + override fun matchesSafely(view: View): Boolean = view.height == pixels + } + + fun hasMinWidth(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("has a minimum width of ${pixels}px") + } + + override fun matchesSafely(view: View): Boolean = view.width >= pixels + } + + fun hasMinHeight(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("has a minimum height of ${pixels}px") + } + + override fun matchesSafely(view: View): Boolean = view.height >= pixels + } +} + +fun ViewInteraction.assertLineCount(lineCount: Int) { + val matcher = object : TypeSafeMatcher() { + override fun matchesSafely(item: View): Boolean { + return (item as TextView).lineCount == lineCount + } + + override fun describeTo(description: Description) { + description.appendText("isTextInLines") + } + } + check(matches(matcher)) +} + + +fun ViewInteraction.getView(): View { + lateinit var matchingView: View + perform(object : ViewAction { + override fun getDescription() = "Get View reference" + + override fun getConstraints(): Matcher { + return isAssignableFrom(View::class.java) + } + + override fun perform(uiController: UiController?, view: View) { + matchingView = view + } + }) + return matchingView +} + +fun ViewInteraction.assertCompletelyAbove(other: ViewInteraction) { + val view1 = getView() + val view2 = other.getView() + val location1 = view1.locationOnScreen + val location2 = view2.locationOnScreen + val isAbove = location1[1] + view1.height <= location2[1] + assertThat("completely above", isAbove, `is`(true)) +} + +fun ViewInteraction.assertCompletelyBelow(other: ViewInteraction) { + val view1 = getView() + val view2 = other.getView() + val location1 = view1.locationOnScreen + val location2 = view2.locationOnScreen + val isAbove = location2[1] + view2.height <= location1[1] + assertThat("completely below", isAbove, `is`(true)) +} + +val View.locationOnScreen get() = IntArray(2).apply { getLocationOnScreen(this) } + + +/** + * Asserts that the TextView uses the specified font size in scaled pixels + */ +fun ViewInteraction.assertFontSizeSP(expectedSP: Float) { + val matcher = object : TypeSafeMatcher(View::class.java) { + + override fun matchesSafely(target: View): Boolean { + if (target !is TextView) return false + val actualSP = target.textSize / target.getResources().displayMetrics.scaledDensity + return actualSP.compareTo(expectedSP) == 0 + } + + override fun describeTo(description: Description) { + description.appendText("with fontSize: ${expectedSP}px") + } + } + check(matches(matcher)) +} + +fun ViewInteraction.assertIsRefreshing(isRefreshing: Boolean) { + val matcher = object : BoundedMatcher(SwipeRefreshLayout::class.java) { + + override fun describeTo(description: Description) { + description.appendText(if (isRefreshing) "is refreshing" else "is not refreshing") + } + + override fun matchesSafely(view: SwipeRefreshLayout): Boolean { + return view.isRefreshing == isRefreshing + } + } + check(matches(matcher)) +} + +class IntentActionMatcher(private val intentType: String, private val dataMatcher: String) : TypeSafeMatcher() { + + override fun describeTo(description: Description?) { + description?.appendText("Intent Matcher") + } + + override fun matchesSafely(item: Intent?): Boolean { + return (intentType == item?.action) && (item?.dataString?.contains(dataMatcher) ?: false) + } +} + +// Adapted from https://medium.com/@dbottillo/android-ui-test-espresso-matcher-for-imageview-1a28c832626f +/** + * Matches ImageView (or ImageButton) with the drawable associated with [resourceId]. If [resourceId] < 0, will + * match against "no drawable" / "drawable is null". + * + * If the [color] param is non-null, then the drawable associated with [resourceId] will be colored + * prior to matching. + */ +class ImageViewDrawableMatcher(val resourceId: Int, val color: Int? = null) : TypeSafeMatcher( + ImageView::class.java) { + override fun describeTo(description: Description) { + description.appendText("with drawable from resource id: ") + description.appendValue(resourceId) + } + + override fun matchesSafely(target: View?): Boolean { + if (target !is ImageView) { + return false + } + val imageView = target + if (resourceId < 0) { + return imageView.drawable == null + } + val resources: Resources = target.getContext().getResources() + val expectedDrawable: Drawable = resources.getDrawable(resourceId) ?: return false + if(color != null) { + ColorUtils.colorIt(color, expectedDrawable) + } + val bitmap: Bitmap = getBitmap(imageView.getDrawable()) + val otherBitmap: Bitmap = getBitmap(expectedDrawable) + return bitmap.sameAs(otherBitmap) + } + + private fun getBitmap(drawable: Drawable): Bitmap { + val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, + drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()) + drawable.draw(canvas) + return bitmap + } +} diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt index 138b08ca27..9e77a41f1f 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt @@ -52,7 +52,7 @@ open class TestAppManager: AppManager() { val config = Configuration.Builder() .setMinimumLoggingLevel(Log.DEBUG) .setExecutor(SynchronousExecutor()) - .setWorkerFactory(getWorkManagerFactory()) + .setWorkerFactory(this.getWorkManagerFactory()) .build() WorkManagerTestInitHelper.initializeTestWorkManager(context, config) testDriver = WorkManagerTestInitHelper.getTestDriver(context) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/E2EAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/E2EAnnotation.kt similarity index 80% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/E2EAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/E2EAnnotation.kt index 02533bb941..98ada4bb68 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/E2EAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/E2EAnnotation.kt @@ -1,4 +1,4 @@ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test will run "end-to-end", generating real network requests @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/FlakyE2EAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/FlakyE2EAnnotation.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/FlakyE2EAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/FlakyE2EAnnotation.kt index 7d2f3799dc..eab0993572 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/FlakyE2EAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/FlakyE2EAnnotation.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out and not yet implemented only for landscape tests. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/KnownBug.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/KnownBug.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/KnownBug.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/KnownBug.kt index d471ef40ea..1e3afc979b 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/KnownBug.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/KnownBug.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out and not yet implemented only for landscape tests. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/OfflineE2E.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/OfflineE2E.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/OfflineE2E.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/OfflineE2E.kt index bd8864ec0c..96534c2841 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/OfflineE2E.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/OfflineE2E.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is the part of the Offline mode test case suite. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/ReleaseExclude.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/ReleaseExclude.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/ReleaseExclude.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/ReleaseExclude.kt index 382ce737a4..7beface496 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/ReleaseExclude.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/ReleaseExclude.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out from the release process because of 3rd party flakiness or any other reason (explained in the parameter). @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/RunsOnTablet.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/RunsOnTablet.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/RunsOnTablet.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/RunsOnTablet.kt index 95e546fb23..d671d6379f 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/RunsOnTablet.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/RunsOnTablet.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, it will surely run within the tablet FTL workflow @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubAnnotation.kt similarity index 81% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubAnnotation.kt index 2daf174572..debf2733e4 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubAnnotation.kt @@ -1,4 +1,4 @@ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out and not yet implemented @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubCoverageAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubCoverageAnnotation.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubCoverageAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubCoverageAnnotation.kt index 66d7c43636..e329c0fca4 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubCoverageAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubCoverageAnnotation.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out from the coverage workflow. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubLandscapeAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubLandscapeAnnotation.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubLandscapeAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubLandscapeAnnotation.kt index a72f017302..1e5c97f7b1 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubLandscapeAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubLandscapeAnnotation.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out and not yet implemented only for landscape tests. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubMultiAPILevel.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubMultiAPILevel.kt index 9a4b28306c..1a20bec7a2 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubMultiAPILevel.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // Apply on a test method which is failing on Firebase Test Lab (FTL) on some API levels. Write the failing API Levels into the parameter. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubTabletAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubTabletAnnotation.kt similarity index 93% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubTabletAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubTabletAnnotation.kt index a4b73da2b9..c2c632c993 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubTabletAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubTabletAnnotation.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt index 8c431f4240..86f412c8b8 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt @@ -16,19 +16,18 @@ package com.instructure.canvas.espresso.common.interaction import com.instructure.canvas.espresso.CanvasComposeTest -import com.instructure.canvas.espresso.StubLandscape -import com.instructure.canvas.espresso.StubTablet +import com.instructure.canvas.espresso.annotations.StubLandscape import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage import com.instructure.canvas.espresso.common.pages.compose.CalendarFilterPage import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToAssignment -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addPlannable -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToAssignment +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addPlannable +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.PlannableType import com.instructure.canvasapi2.models.Quiz diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt index 4f0617765f..6879db01d8 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt @@ -21,8 +21,8 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.CalendarEventCreateEditPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addUserCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addUserCalendarEvent import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.DateHelper diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt index 9946c1ecce..1408ff5037 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt @@ -21,8 +21,8 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoCreateUpdatePage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addPlannable +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addPlannable import com.instructure.canvasapi2.models.PlannableType import com.instructure.canvasapi2.models.User import org.junit.Test diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/EventDetailsInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/EventDetailsInteractionTest.kt index 398966cb6d..04fd8546d9 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/EventDetailsInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/EventDetailsInteractionTest.kt @@ -21,8 +21,8 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.CalendarEventCreateEditPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.toApiString diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt index cd6efb603d..0412d2214e 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt @@ -18,12 +18,12 @@ package com.instructure.canvas.espresso.common.interaction import com.instructure.canvas.espresso.CanvasComposeTest -import com.instructure.canvas.espresso.StubLandscape -import com.instructure.canvas.espresso.StubTablet +import com.instructure.canvas.espresso.annotations.StubLandscape +import com.instructure.canvas.espresso.annotations.StubTablet import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage import com.instructure.canvas.espresso.common.pages.compose.GradesPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addGradingPeriod +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addGradingPeriod import com.instructure.canvasapi2.models.GradingPeriod import com.instructure.canvasapi2.utils.NumberHelper import com.instructure.espresso.ModuleItemInteractions diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt index b1aa69f95c..257f2f802d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt @@ -5,8 +5,8 @@ import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.canvas.espresso.common.pages.compose.InboxComposePage import com.instructure.canvas.espresso.common.pages.compose.RecipientPickerPage import com.instructure.canvas.espresso.common.pages.compose.SelectContextPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addSentConversation +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addSentConversation import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Course diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxDetailsInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxDetailsInteractionTest.kt index 654ac4a995..dccf2f00f8 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxDetailsInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxDetailsInteractionTest.kt @@ -19,8 +19,8 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.canvas.espresso.common.pages.compose.InboxComposePage import com.instructure.canvas.espresso.common.pages.compose.InboxDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.utils.Randomizer import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.User import org.junit.Test diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt index 2d5709303f..806f1af8c7 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt @@ -22,11 +22,11 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.common.pages.InboxPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addConversation -import com.instructure.canvas.espresso.mockCanvas.addConversations -import com.instructure.canvas.espresso.mockCanvas.addConversationsToCourseMap -import com.instructure.canvas.espresso.mockCanvas.createBasicConversation +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addConversation +import com.instructure.canvas.espresso.mockcanvas.addConversations +import com.instructure.canvas.espresso.mockcanvas.addConversationsToCourseMap +import com.instructure.canvas.espresso.mockcanvas.createBasicConversation import com.instructure.canvas.espresso.refresh import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.User diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxSignatureInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxSignatureInteractionTest.kt index 83e2e5cde5..f170ca9f1d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxSignatureInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxSignatureInteractionTest.kt @@ -18,7 +18,7 @@ package com.instructure.canvas.espresso.common.interaction import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.InboxSignatureSettingsPage import com.instructure.canvas.espresso.common.pages.compose.SettingsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import org.junit.Test abstract class InboxSignatureInteractionTest : CanvasComposeTest() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt index 24fb85ea8d..97d7214018 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt @@ -16,7 +16,7 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.SettingsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import org.junit.Test abstract class SettingsInteractionTest : CanvasComposeTest() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt index 6c801164a8..e9e68a81c4 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt @@ -19,10 +19,10 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage import com.instructure.canvas.espresso.common.pages.compose.SmartSearchPage import com.instructure.canvas.espresso.common.pages.compose.SmartSearchPreferencesPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addPageToCourse +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addPageToCourse import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Page import com.instructure.canvasapi2.models.SmartSearchContentType diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/ToDoDetailsInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/ToDoDetailsInteractionTest.kt index 92a94d0deb..3dbaf29704 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/ToDoDetailsInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/ToDoDetailsInteractionTest.kt @@ -22,8 +22,8 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoCreateUpdatePage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addPlannable +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addPlannable import com.instructure.canvasapi2.models.PlannableType import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.toDate diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/ReminderPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentReminderPage.kt similarity index 98% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/ReminderPage.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentReminderPage.kt index 9523ceba2d..9867a17115 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/ReminderPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentReminderPage.kt @@ -53,7 +53,7 @@ import org.hamcrest.Matchers.anything import java.util.Calendar -class ReminderPage(private val composeTestRule: ComposeTestRule) { +class AssignmentReminderPage(private val composeTestRule: ComposeTestRule) { private val reminderTitle = "Reminder" private val reminderDescription = "Add due date reminder notifications about this assignment on this device." private val reminderAdd = "Add reminder" diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/GradesPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/GradesPage.kt index 550feb2e45..331db6eb0e 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/GradesPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/GradesPage.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import androidx.compose.ui.test.swipeUp -import com.instructure.composeTest.hasDrawable +import com.instructure.composetest.hasDrawable import com.instructure.pandautils.R diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/Endpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/Endpoint.kt similarity index 91% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/Endpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/Endpoint.kt index 1f8b364b21..a5b60b11d0 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/Endpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/Endpoint.kt @@ -16,11 +16,17 @@ */ @file:Suppress("unused") -package com.instructure.canvas.espresso.mockCanvas +package com.instructure.canvas.espresso.mockcanvas -import com.instructure.canvas.espresso.mockCanvas.utils.* -import okhttp3.* +import com.instructure.canvas.espresso.mockcanvas.utils.AuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.CanvasAuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.SegmentQualifier +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody /** diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/HttpResponder.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/HttpResponder.kt similarity index 88% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/HttpResponder.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/HttpResponder.kt index eccbff1aea..2231e1f695 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/HttpResponder.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/HttpResponder.kt @@ -14,9 +14,9 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas +package com.instructure.canvas.espresso.mockcanvas -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars import okhttp3.Request import okhttp3.Response @@ -32,6 +32,7 @@ class HttpResponder( ) { private var getMethod: (() -> Response)? = null + private var headMethod: (() -> Response)? = null private var postMethod: (() -> Response)? = null private var putMethod: (() -> Response)? = null private var deleteMethod: (() -> Response)? = null @@ -40,6 +41,10 @@ class HttpResponder( getMethod = onHandle } + fun HttpResponder.HEAD(onHandle: () -> Response) { + headMethod = onHandle + } + fun HttpResponder.POST(onHandle: () -> Response) { postMethod = onHandle } @@ -56,6 +61,7 @@ class HttpResponder( val method = request.method return when { method == "GET" && getMethod != null -> getMethod!!() + method == "HEAD" && headMethod != null -> headMethod!!() method == "POST" && postMethod != null -> postMethod!!() method == "PUT" && putMethod != null -> putMethod!!() method == "DELETE" && deleteMethod != null -> deleteMethod!!() diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt similarity index 99% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt index 19a1659711..a13070d671 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt @@ -16,11 +16,11 @@ */ @file:Suppress("unused") -package com.instructure.canvas.espresso.mockCanvas +package com.instructure.canvas.espresso.mockcanvas import android.util.Log import com.github.javafaker.Faker -import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer +import com.instructure.canvas.espresso.mockcanvas.utils.Randomizer import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.models.Account import com.instructure.canvasapi2.models.AccountNotification diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvasInterceptor.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvasInterceptor.kt similarity index 88% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvasInterceptor.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvasInterceptor.kt index ec50d41c5e..b402ba5277 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvasInterceptor.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvasInterceptor.kt @@ -14,10 +14,10 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas +package com.instructure.canvas.espresso.mockcanvas -import com.instructure.canvas.espresso.mockCanvas.endpoints.RootEndpoint -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.endpoints.RootEndpoint +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars import okhttp3.Interceptor import okhttp3.Response diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AccountEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AccountEndpoints.kt similarity index 90% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AccountEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AccountEndpoints.kt index 9aae21cc58..e54ec630c3 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AccountEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AccountEndpoints.kt @@ -14,12 +14,24 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.* -import com.instructure.canvasapi2.models.* +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.AccountId +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.UserId +import com.instructure.canvas.espresso.mockcanvas.utils.successPaginatedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvasapi2.models.Account +import com.instructure.canvasapi2.models.AccountNotification +import com.instructure.canvasapi2.models.BecomeUserPermission +import com.instructure.canvasapi2.models.HelpLink +import com.instructure.canvasapi2.models.HelpLinks +import com.instructure.canvasapi2.models.LaunchDefinition /** * Endpoint that can return a list of [Account]s. We currently assume that only one account is supported at a time and that diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt index d63c62dfc2..ee685b23fc 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt @@ -14,25 +14,25 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints import android.util.Log import com.google.gson.Gson -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addPlannable -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.DontCareAuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.StringId -import com.instructure.canvas.espresso.mockCanvas.utils.getJsonFromRequestBody -import com.instructure.canvas.espresso.mockCanvas.utils.grabJsonFromMultiPartBody -import com.instructure.canvas.espresso.mockCanvas.utils.successRedirectWithHeader -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.user +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addPlannable +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.DontCareAuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.StringId +import com.instructure.canvas.espresso.mockcanvas.utils.getJsonFromRequestBody +import com.instructure.canvas.espresso.mockcanvas.utils.grabJsonFromMultiPartBody +import com.instructure.canvas.espresso.mockcanvas.utils.successRedirectWithHeader +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.user import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.ModuleContentDetails diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AssignmentEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AssignmentEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt index 114413018a..1aa91e039c 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AssignmentEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt @@ -13,19 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.AssignmentGroup import com.instructure.canvasapi2.models.GradeableStudent import com.instructure.canvasapi2.models.ObserveeAssignment -import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.ObserveeAssignmentGroup import com.instructure.canvasapi2.models.SubmissionSummary import okio.Buffer diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CanvadocEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CanvadocEndpoints.kt similarity index 83% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CanvadocEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CanvadocEndpoints.kt index 62178d4539..988ecd8299 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CanvadocEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CanvadocEndpoints.kt @@ -13,11 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.* +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.AnnotationId +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse import com.instructure.canvasapi2.models.canvadocs.CanvaDocAnnotationResponse /** diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ConversationEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ConversationEndpoints.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ConversationEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ConversationEndpoints.kt index 470f4519e5..592c62c1fb 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ConversationEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ConversationEndpoints.kt @@ -14,12 +14,17 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.* +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.user import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Message import com.instructure.canvasapi2.models.UnreadConversationCount @@ -27,7 +32,7 @@ import com.instructure.canvasapi2.utils.APIHelper import okhttp3.FormBody import okhttp3.Request import okhttp3.Response -import java.util.* +import java.util.GregorianCalendar /** * Endpoint that can return a list of [Conversation]s diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CourseEndpoints.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CourseEndpoints.kt index 31908158c0..8c351bf16c 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CourseEndpoints.kt @@ -14,31 +14,31 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints import android.util.Log import com.google.gson.Gson -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addQuizSubmission -import com.instructure.canvas.espresso.mockCanvas.addReplyToDiscussion -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.AuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.DontCareAuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.StringId -import com.instructure.canvas.espresso.mockCanvas.utils.UserId -import com.instructure.canvas.espresso.mockCanvas.utils.getJsonFromRequestBody -import com.instructure.canvas.espresso.mockCanvas.utils.grabJsonFromMultiPartBody -import com.instructure.canvas.espresso.mockCanvas.utils.noContentResponse -import com.instructure.canvas.espresso.mockCanvas.utils.successPaginatedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.user +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addQuizSubmission +import com.instructure.canvas.espresso.mockcanvas.addReplyToDiscussion +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.AuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.DontCareAuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.StringId +import com.instructure.canvas.espresso.mockcanvas.utils.UserId +import com.instructure.canvas.espresso.mockcanvas.utils.getJsonFromRequestBody +import com.instructure.canvas.espresso.mockcanvas.utils.grabJsonFromMultiPartBody +import com.instructure.canvas.espresso.mockcanvas.utils.noContentResponse +import com.instructure.canvas.espresso.mockcanvas.utils.successPaginatedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.user import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CoursepermissionsEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CoursepermissionsEndpoint.kt similarity index 81% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CoursepermissionsEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CoursepermissionsEndpoint.kt index 1e522a9061..be4fce3d98 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CoursepermissionsEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CoursepermissionsEndpoint.kt @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse /** * Endpoint that can return a CanvasContextPermission value based on the course id diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/EnrollmentEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/EnrollmentEndpoint.kt similarity index 82% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/EnrollmentEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/EnrollmentEndpoint.kt index 333f7756b2..e3b824e963 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/EnrollmentEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/EnrollmentEndpoint.kt @@ -14,14 +14,13 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.MockCanvas.Companion.data -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse import com.instructure.canvasapi2.apis.EnrollmentAPI object EnrollmentIndexEndpoint : Endpoint( diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ExternalToolsEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ExternalToolsEndpoints.kt similarity index 78% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ExternalToolsEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ExternalToolsEndpoints.kt index 7529f99f3e..841a377323 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ExternalToolsEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ExternalToolsEndpoints.kt @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse object ExternalToolsEndpoint : Endpoint( response = { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FileEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FileEndpoints.kt similarity index 65% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FileEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FileEndpoints.kt index add639fa4d..c204e5e7b1 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FileEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FileEndpoints.kt @@ -14,19 +14,17 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import android.util.Log -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.AuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.DontCareAuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.successResponseRaw -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.AuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.DontCareAuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponseRaw +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse /** * Endpoint for file list operations @@ -56,6 +54,15 @@ object FileDownloadEndpoint : Endpoint ( request.unauthorizedResponse() } } + HEAD { + val fileId = pathVars.fileId + val content = data.fileContents[fileId] + if (content != null) { + request.successResponseRaw(content) + } else { + request.unauthorizedResponse() + } + } } diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FolderEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FolderEndpoints.kt similarity index 80% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FolderEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FolderEndpoints.kt index 58bef282c1..cb2eac1d74 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FolderEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FolderEndpoints.kt @@ -14,18 +14,17 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import android.util.Log -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.AuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.DontCareAuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successPaginatedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.AuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.DontCareAuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successPaginatedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse import com.instructure.canvasapi2.models.FileFolder /** diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/MiscEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/MiscEndpoints.kt similarity index 85% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/MiscEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/MiscEndpoints.kt index c031049a15..65066de9b0 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/MiscEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/MiscEndpoints.kt @@ -14,14 +14,13 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.utils.successPaginatedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.successPaginatedResponse import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.models.DashboardCard -import java.util.* +import java.util.Date /** * Endpoint that can return a list of DashboardCards for the request user diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/OAuthEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/OAuthEndpoint.kt similarity index 86% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/OAuthEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/OAuthEndpoint.kt index bf84a6181d..a3064aadb0 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/OAuthEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/OAuthEndpoint.kt @@ -14,9 +14,9 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.Endpoint /** * Endpoint for handling OAuth-related requests diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ObserverAlertsEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ObserverAlertsEndpoint.kt similarity index 82% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ObserverAlertsEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ObserverAlertsEndpoint.kt index 616df09ae3..591aaa4e37 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ObserverAlertsEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ObserverAlertsEndpoint.kt @@ -12,14 +12,14 @@ * 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.canvas.espresso.mockCanvas.endpoints + */ package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.StringId -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.StringId +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse import com.instructure.canvasapi2.models.Alert import com.instructure.canvasapi2.models.AlertWorkflowState diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/RootEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/RootEndpoint.kt similarity index 86% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/RootEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/RootEndpoint.kt index e3eb5f962d..c86481caf4 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/RootEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/RootEndpoint.kt @@ -1,12 +1,12 @@ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints import android.util.Log -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse import com.instructure.canvasapi2.models.AuthenticatedSession import okhttp3.Request import okhttp3.Response diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SearchEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SearchEndpoint.kt similarity index 88% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SearchEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SearchEndpoint.kt index 384150a8ab..b331181483 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SearchEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SearchEndpoint.kt @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse /** * Base endpoint for searches diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SubmissionEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SubmissionEndpoints.kt similarity index 95% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SubmissionEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SubmissionEndpoints.kt index b0bd98abbb..f10343f06e 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SubmissionEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SubmissionEndpoints.kt @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints import android.util.Log -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.UserId -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.user +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.UserId +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.user import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Author import com.instructure.canvasapi2.models.FileUploadParams diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/UserEndpoints.kt similarity index 95% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/UserEndpoints.kt index 89c10000ec..96c8b9ecd9 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/UserEndpoints.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints - -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.addEnrollment -import com.instructure.canvas.espresso.mockCanvas.addFileToFolder -import com.instructure.canvas.espresso.mockCanvas.addObserverAlertThreshold -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.UserId -import com.instructure.canvas.espresso.mockCanvas.utils.successPaginatedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.user +package com.instructure.canvas.espresso.mockcanvas.endpoints + +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.addEnrollment +import com.instructure.canvas.espresso.mockcanvas.addFileToFolder +import com.instructure.canvas.espresso.mockcanvas.addObserverAlertThreshold +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.UserId +import com.instructure.canvas.espresso.mockcanvas.utils.successPaginatedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.user import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Bookmark import com.instructure.canvasapi2.models.CanvasContext diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeAssignmentDetailsManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeAssignmentDetailsManager.kt similarity index 92% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeAssignmentDetailsManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeAssignmentDetailsManager.kt index c894c0c6dc..5638887a9d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeAssignmentDetailsManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeAssignmentDetailsManager.kt @@ -12,9 +12,9 @@ * 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.canvas.espresso.mockCanvas.fakes + */package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.AssignmentDetailsQuery import com.instructure.canvasapi2.managers.graphql.AssignmentDetailsManager diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCommentLibraryManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCommentLibraryManager.kt similarity index 89% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCommentLibraryManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCommentLibraryManager.kt index 2d8d6180dd..bdc9cd200b 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCommentLibraryManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCommentLibraryManager.kt @@ -14,9 +14,9 @@ * along with this program. If not, see . * */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.managers.CommentLibraryManager class FakeCommentLibraryManager : CommentLibraryManager { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCustomGradeStatusesManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCustomGradeStatusesManager.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCustomGradeStatusesManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCustomGradeStatusesManager.kt index f2441e78ce..f78f42cd73 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCustomGradeStatusesManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCustomGradeStatusesManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes import com.instructure.canvasapi2.CustomGradeStatusesQuery import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeInboxSettingsManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeInboxSettingsManager.kt similarity index 92% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeInboxSettingsManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeInboxSettingsManager.kt index f9d75edff5..f6ade0b4ed 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeInboxSettingsManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeInboxSettingsManager.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.managers.InboxSettingsManager import com.instructure.canvasapi2.managers.InboxSignatureSettings import com.instructure.canvasapi2.utils.DataResult diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakePostPolicyManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakePostPolicyManager.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakePostPolicyManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakePostPolicyManager.kt index 4d0486ca15..69c3becfa0 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakePostPolicyManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakePostPolicyManager.kt @@ -12,7 +12,7 @@ * 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.canvas.espresso.mockCanvas.fakes + */package com.instructure.canvas.espresso.mockcanvas.fakes import com.instructure.canvasapi2.HideAssignmentGradesForSectionsMutation import com.instructure.canvasapi2.HideAssignmentGradesMutation diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeStudentContextManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeStudentContextManager.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeStudentContextManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeStudentContextManager.kt index 08b7523101..22f18cc65a 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeStudentContextManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeStudentContextManager.kt @@ -14,9 +14,9 @@ * along with this program. If not, see . * */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.StudentContextCardQuery import com.instructure.canvasapi2.managers.StudentContextManager import com.instructure.canvasapi2.type.AssignmentState diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionCommentsManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionCommentsManager.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionCommentsManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionCommentsManager.kt index 42654b20c1..1b18558644 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionCommentsManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionCommentsManager.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes import com.instructure.canvasapi2.CreateSubmissionCommentMutation import com.instructure.canvasapi2.SubmissionCommentsQuery diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionContentManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionContentManager.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionContentManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionContentManager.kt index 648610459b..34fee6460f 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionContentManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionContentManager.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.SubmissionContentQuery import com.instructure.canvasapi2.fragment.SubmissionFields import com.instructure.canvasapi2.managers.graphql.SubmissionContentManager diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionDetailsManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionDetailsManager.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionDetailsManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionDetailsManager.kt index 5e1c023c7b..e99be5fda8 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionDetailsManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionDetailsManager.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.SubmissionDetailsQuery import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.type.SubmissionType diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionGradeManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionGradeManager.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionGradeManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionGradeManager.kt index f4b6b8823f..916c727b6c 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionGradeManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionGradeManager.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.SubmissionGradeQuery import com.instructure.canvasapi2.UpdateSubmissionGradeMutation import com.instructure.canvasapi2.UpdateSubmissionStatusMutation diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionRubricManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionRubricManager.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionRubricManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionRubricManager.kt index a6a54f4ece..303a92b766 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionRubricManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionRubricManager.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.SubmissionRubricQuery import com.instructure.canvasapi2.managers.SubmissionRubricManager diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/AuthUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/AuthUtils.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/AuthUtils.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/AuthUtils.kt index 42fb9644eb..fee4537a78 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/AuthUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/AuthUtils.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.utils +package com.instructure.canvas.espresso.mockcanvas.utils import com.instructure.canvasapi2.utils.validOrNull import okhttp3.Request diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/PathUtils.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/PathUtils.kt index 85332493f7..0c4cf75ad4 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/PathUtils.kt @@ -14,9 +14,9 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.utils +package com.instructure.canvas.espresso.mockcanvas.utils -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import okhttp3.Request import java.net.URLEncoder import kotlin.reflect.KMutableProperty1 diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/Randomizer.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/Randomizer.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/Randomizer.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/Randomizer.kt index faf823f0a4..d981765e1e 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/Randomizer.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/Randomizer.kt @@ -16,10 +16,11 @@ */ @file:Suppress("unused") -package com.instructure.canvas.espresso.mockCanvas.utils +package com.instructure.canvas.espresso.mockcanvas.utils import com.github.javafaker.Faker -import java.util.* +import java.util.Date +import java.util.UUID /** * Provides access to numerous convenience functions for generating random data diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/RequestUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/RequestUtils.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/RequestUtils.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/RequestUtils.kt index cda02e7865..ea9cab764a 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/RequestUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/RequestUtils.kt @@ -14,15 +14,18 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.utils +package com.instructure.canvas.espresso.mockcanvas.utils import android.util.Log import com.google.gson.Gson import com.google.gson.JsonObject -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.models.User -import okhttp3.* import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import okio.Buffer import okio.IOException diff --git a/automation/espresso/src/main/kotlin/com/instructure/composeTest/ComposeCustomMatchers.kt b/automation/espresso/src/main/kotlin/com/instructure/composetest/ComposeCustomMatchers.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/composeTest/ComposeCustomMatchers.kt rename to automation/espresso/src/main/kotlin/com/instructure/composetest/ComposeCustomMatchers.kt index fc1d40bd4b..868977b0d1 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/composeTest/ComposeCustomMatchers.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/composetest/ComposeCustomMatchers.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.composeTest +package com.instructure.composetest import androidx.annotation.DrawableRes import androidx.compose.ui.semantics.getOrNull diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt index 6e4364dc92..3bb02d11a9 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt @@ -16,6 +16,7 @@ package com.instructure.espresso import android.os.Build +import android.util.Log import android.view.View import androidx.annotation.RequiresApi import androidx.recyclerview.widget.RecyclerView @@ -245,19 +246,52 @@ fun getRecyclerViewFromMatcher(matcher: Matcher): RecyclerView { return recyclerView ?: throw IllegalStateException("Failed to retrieve RecyclerView") } -fun handleWorkManagerTask(workerTag: String) { +fun handleWorkManagerTask(workerTag: String, timeoutMillis: Long = 20000) { val app = ApplicationProvider.getApplicationContext() - val testDriver = app.testDriver!! + var endTime = System.currentTimeMillis() + timeoutMillis + var workInfo: androidx.work.WorkInfo? = null + + var testDriver = app.testDriver + if (testDriver == null) { + while (System.currentTimeMillis() < endTime && app.testDriver == null) { + Log.w("handleWorkManagerTask", "testDriver is null, attempting to initialize WorkManager") + app.initWorkManager(app) + testDriver = app.testDriver + Thread.sleep(500) + } + } + + if (testDriver == null) { + Assert.fail("A TestAppManager.testDriver was null, so was not initialized before the timeout.") + } - val workInfos = WorkManager.getInstance(app) - .getWorkInfosByTag(workerTag) - .get() - val workInfo = workInfos.find { !it.state.isFinished } + endTime = System.currentTimeMillis() + timeoutMillis + while (System.currentTimeMillis() < endTime) { + try { + val workInfos = WorkManager.getInstance(app).getWorkInfosByTag(workerTag).get() + for(work in workInfos) { + Log.w("STUDENT_APP_TAG","WorkInfo: $work") + } + workInfo = workInfos.find { !it.state.isFinished } - testDriver.setAllConstraintsMet(workInfo?.id ?: return) + if (workInfo != null) break + + } catch (e: Exception) { + e.printStackTrace() + } + Thread.sleep(500) + } + + if (workInfo == null) { + val workInfos = WorkManager.getInstance(app).getWorkInfosByTag(workerTag).get() + Assert.fail("Unable to find WorkInfo with tag:'$workerTag' in ${timeoutMillis} ms. WorkInfos found: $workInfos") + } + + testDriver!!.setAllConstraintsMet(workInfo!!.id) waitForWorkManagerJobsToFinish(workerTag = workerTag) } + private fun waitForWorkManagerJobsToFinish(timeoutMs: Long = 20000L, workerTag: String) { val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext val workManager = WorkManager.getInstance(context) diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/ViewUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/ViewUtils.kt deleted file mode 100644 index 4f382966bc..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/ViewUtils.kt +++ /dev/null @@ -1,34 +0,0 @@ -// -// Copyright (C) 2018-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.espresso - -import androidx.test.espresso.util.HumanReadables -import android.view.View -import androidx.test.espresso.Espresso - -object ViewUtils { - - fun toString(view: View): String { - return HumanReadables.getViewHierarchyErrorMessage(view, null, "", null) - } - - fun pressBackButton(times: Int) { - for(i in 1..times) { - Espresso.pressBack() - } - } -} diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P0.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P0.kt deleted file mode 100644 index ac23170c8a..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P0.kt +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2018-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.espresso.filters - -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - -@Retention(RetentionPolicy.RUNTIME) -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS, AnnotationTarget.FILE) -annotation class P0 diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P1.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P1.kt deleted file mode 100644 index 2e15cc3f40..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P1.kt +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2018-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.espresso.filters - -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - -@Retention(RetentionPolicy.RUNTIME) -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS, AnnotationTarget.FILE) -annotation class P1 diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P2.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P2.kt deleted file mode 100644 index 0c16b356dc..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P2.kt +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2018-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.espresso.filters - -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - -@Retention(RetentionPolicy.RUNTIME) -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS, AnnotationTarget.FILE) -annotation class P2 diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P3.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P3.kt deleted file mode 100644 index 22ba81587b..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P3.kt +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2018-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.espresso.filters - -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - -@Retention(RetentionPolicy.RUNTIME) -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS, AnnotationTarget.FILE) -annotation class P3 diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P4.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P4.kt deleted file mode 100644 index a8febaebd3..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P4.kt +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2018-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.espresso.filters - -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - -@Retention(RetentionPolicy.RUNTIME) -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS, AnnotationTarget.FILE) -annotation class P4 diff --git a/libs/pandautils/flank.yml b/libs/pandautils/flank.yml index 60bd72123a..d11ee0d065 100644 --- a/libs/pandautils/flank.yml +++ b/libs/pandautils/flank.yml @@ -8,7 +8,7 @@ gcloud: performance-metrics: false timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: Pixel2.arm version: 29 diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesAssignmentItemTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesAssignmentItemTest.kt index a5a6efcb9a..7f4266ea8e 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesAssignmentItemTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesAssignmentItemTest.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.composeTest.hasDrawable +import com.instructure.composetest.hasDrawable import com.instructure.espresso.assertTextColor import com.instructure.pandares.R import com.instructure.pandautils.features.grades.AssignmentItem diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesScreenTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesScreenTest.kt index 0aedb86e14..ee4a0e5918 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesScreenTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesScreenTest.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.composeTest.hasDrawable +import com.instructure.composetest.hasDrawable import com.instructure.pandares.R import com.instructure.pandautils.compose.composables.DiscussionCheckpointUiState import com.instructure.pandautils.features.grades.AssignmentGroupUiState diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchScreenTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchScreenTest.kt index f7a375f8a0..fb4110765e 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchScreenTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchScreenTest.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.SmartSearchContentType -import com.instructure.composeTest.hasSiblingWithText +import com.instructure.composetest.hasSiblingWithText import com.instructure.pandautils.features.smartsearch.SmartSearchResultUiState import com.instructure.pandautils.features.smartsearch.SmartSearchScreen import com.instructure.pandautils.features.smartsearch.SmartSearchSortType From d6744dbcee44433e0e5c610588de402948e98613 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Fri, 3 Oct 2025 10:54:00 +0200 Subject: [PATCH 17/94] Add agent instructions for Claude Code and GitHub Copilot (#3280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- apps/.github/copilot-instructions.md | 68 +++++++++++ apps/CLAUDE.md | 173 +++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 apps/.github/copilot-instructions.md create mode 100644 apps/CLAUDE.md diff --git a/apps/.github/copilot-instructions.md b/apps/.github/copilot-instructions.md new file mode 100644 index 0000000000..b2ed7148b5 --- /dev/null +++ b/apps/.github/copilot-instructions.md @@ -0,0 +1,68 @@ +# GitHub Copilot Preferences + +## Project Context + +- Canvas is a learning management system with multiple Android apps (Student, Teacher, Parent) +- The canvas-android project consists of multiple packages managed with Gradle +- The apps share a common :pandautils module for shared code +- There is a standalone module for each app (student, teacher, parent) +- The apps are written in Kotlin and use a mix of views with xml and Jetpack Compose for UI +- The apps use Retrofit and OkHttp for networking +- The apps use Room for local database storage +- The apps use Dagger Hilt for dependency injection +- The apps use Coroutines and Flow for asynchronous programming +- The apps use JUnit and Espresso for testing +- The apps follow MVVM architecture pattern +- The apps use Material Design components for UI +- The apps support multiple screen sizes and orientations +- The apps support dark mode +- The apps support multiple languages and localization + +## Response Preferences + +- Be concise and prioritize code examples +- Suggest Kotlin solutions with proper typing +- When suggesting code, ensure it follows existing project patterns and conventions +- When explaining code, focus on implementation details and potential edge cases +- When suggesting libraries or tools, ensure they are compatible with existing project dependencies and architecture +- When suggesting architectural changes, consider the impact on existing code and dependencies +- When suggesting UI changes, consider accessibility and responsiveness +- When suggesting testing strategies, consider existing test coverage and frameworks used +- Use Kotlin, Jetpack Compose and Kotlin Coroutines best practices appropriately +- Reference existing project patterns when suggesting new implementations +- Always use test you can see the output of. + +## Tool Usage Preferences + +- Prefer searching through codebase before suggesting solutions +- Use error checking after code edits +- When given a ticket identifier, use Atlassian MCP server to gather information about the task +- When given a Figma link, use Figma Desktop MCP server to gather design details + +## Code Style Preferences +- Follow existing project code style and conventions +- Use consistent indentation and spacing +- Use descriptive variable and function names +- Prefer immutability where possible +- Use Kotlin idioms and best practices +- Avoid unnecessary complexity +- Ensure code is modular and reusable +- Follow SOLID principles +- Ensure proper error handling and logging +- Write unit tests for new functionality +- Write integration tests for new functionality +- Write UI tests for new functionality +- Ensure tests are isolated and repeatable +- Use mocking frameworks for dependencies in tests +- Ensure code coverage is maintained or improved with new changes +- When suggesting code snippets, ensure they are complete and can be directly used or easily integrated +- DO NOT add comments or documentation unless its specifically requested +- Code should be self-explanatory without inline comments +- When generating test files, only include license headers and the actual test code without explanatory comments + +## Implementation Preferences +- Follow project's component structure and naming conventions +- Use existing utility functions and shared components when possible +- Ensure the code compiles and runs without errors +- When writing tests, make sure the tests pass +- When you are asked to write tests, ensure they are written in the same manner as existing tests in the project \ No newline at end of file diff --git a/apps/CLAUDE.md b/apps/CLAUDE.md new file mode 100644 index 0000000000..da8bd2a081 --- /dev/null +++ b/apps/CLAUDE.md @@ -0,0 +1,173 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Canvas Android is a multi-app learning management system project with three main applications (Student, Teacher, Parent) sharing common libraries. The apps are built with Kotlin, Jetpack Compose (modern UI) and XML layouts (legacy), following MVVM architecture with Dagger Hilt for dependency injection. + +**Main Applications:** +- `student/` - Canvas Student app for students +- `teacher/` - Canvas Teacher app for teachers +- `parent/` - Canvas Parent app for parents (native version; there's also a Flutter version at `flutter_parent/`) + +**Shared Libraries (in `../libs/`):** +- `pandautils/` - Core shared library with common code, features, compose components, and utilities +- `canvas-api-2/` - Canvas LMS API client using Retrofit/OkHttp +- `login-api-2/` - Authentication and login flows +- `annotations/` - PSPDFKit wrapper for PDF annotation handling +- `rceditor/` - Rich content editor wrapper +- `interactions/` - Navigation interactions +- `horizon/` - Career experience features +- `pandares/` - Shared resources + +**Testing Libraries (in `../automation/`):** +- `espresso/` - UI testing framework built on Espresso +- `dataseedingapi/` - gRPC wrapper for Canvas data seeding in tests + +## Build Commands + +Run from repository root (`canvas-android/`), not the `apps/` directory: + +```bash +# Build Student app (dev debug variant) +./gradle/gradlew -p apps :student:assembleDevDebug + +# Build Teacher app (dev debug variant) +./gradle/gradlew -p apps :teacher:assembleDevDebug + +# Build Parent app (dev debug variant) +./gradle/gradlew -p apps :parent:assembleDevDebug + +# Build all apps +./gradle/gradlew -p apps assembleAllApps + +# Clean build +./gradle/gradlew -p apps clean +``` + +## Running Tests + +**Unit Tests:** +1. Set Build Variant to `qaDebug` in Android Studio +2. Run tests by clicking the play button next to test cases/classes +3. Or via command line: +```bash +./gradle/gradlew -p apps :student:testQaDebugUnitTest +./gradle/gradlew -p apps :teacher:testQaDebugUnitTest +./gradle/gradlew -p apps :parent:testQaDebugUnitTest +``` + +**Instrumentation/Espresso Tests:** +```bash +./gradle/gradlew -p apps :student:connectedQaDebugAndroidTest +./gradle/gradlew -p apps :teacher:connectedQaDebugAndroidTest +``` + +**Single Test:** +```bash +# Unit test +./gradle/gradlew -p apps :student:testQaDebugUnitTest --tests "com.instructure.student.SpecificTest" + +# Instrumentation test +./gradle/gradlew -p apps :student:connectedQaDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.instructure.student.ui.SpecificTest +``` + +## Architecture + +### Feature Organization + +Features follow a modular structure within each app under `src/main/java/com/instructure/{app}/features/{feature}/`: + +``` +features/ + └── featurename/ + ├── FeatureScreen.kt # Compose UI (modern) + ├── FeatureFragment.kt # Fragment (legacy) + ├── FeatureViewModel.kt # Business logic + ├── FeatureRepository.kt # Data layer + └── FeatureUiState.kt # UI state models +``` + +**Test Structure:** +- Unit tests: `src/test/java/com/instructure/{app}/features/{feature}/` +- Instrumentation tests: `src/androidTest/java/com/instructure/{app}/ui/{feature}/` + +### MVVM Pattern + +- **ViewModels** handle business logic and state management using Kotlin Flows/LiveData +- **Repositories** abstract data sources (API, local database, etc.) +- **UI Layer** - Jetpack Compose for new features, XML + Data Binding for legacy +- **Dependency Injection** - Dagger Hilt modules in `di/` directories + +### Key Technologies + +- **UI**: Jetpack Compose (modern), XML layouts + Data Binding (legacy), Material Design 3 +- **Networking**: Retrofit 3.0, OkHttp 5.1, Apollo GraphQL 4.3 +- **Database**: Room 2.8 with Coroutines +- **DI**: Dagger Hilt 2.57 +- **Async**: Kotlin Coroutines 1.9, Flow, LiveData +- **Testing**: JUnit 4, Mockk, Robolectric, Espresso, Compose UI Testing +- **Other**: Mobius (for some features), WorkManager, Firebase (Crashlytics, Messaging) + +### Build Configuration + +- **Build Tools**: AGP 8.13, Kotlin 2.2, KSP 2.0 +- **SDK**: compileSdk 35, minSdk 28, targetSdk 35 +- **Java**: Version 17 +- **Build Variants**: + - Flavors: `dev`, `qa`, `prod` + - Types: `debug`, `debugMinify`, `release` + - Common: `devDebug` (development), `qaDebug` (testing) + +Dependencies are centralized in `buildSrc/src/main/java/GlobalDependencies.kt` with `Versions`, `Libs`, and `Plugins` objects. + +## Development Guidelines + +### Code Style +- Use Kotlin idioms and best practices +- Prefer immutability where possible +- Follow existing project patterns and conventions +- Self-documenting code without inline comments unless specifically requested +- Use descriptive variable and function names + +### Component Patterns +- Use existing utility functions and shared components from `pandautils` +- Follow project's component structure and naming conventions +- Prefer Repository pattern for data access +- Use Hilt for dependency injection +- New UI features should use Jetpack Compose +- Legacy features may use XML + Data Binding + +### Testing Patterns +- Write unit tests in the same manner as existing tests (e.g., check `student/src/test/`) +- Write instrumentation tests in the same manner as existing tests (e.g., check `student/src/androidTest/`) +- Mock dependencies with Mockk +- Use test doubles for repositories in ViewModel tests +- Espresso tests should use page object pattern from `:espresso` module +- Ensure tests are isolated and repeatable + +### Module Dependencies +- Apps depend on shared libraries (`:pandautils`, `:canvas-api-2`, etc.) +- Shared libraries are in `../libs/` relative to `apps/` +- Canvas API models and endpoints are in `:canvas-api-2` +- Common utilities, dialogs, and base classes are in `:pandautils` + +## Additional Context + +### Initial Setup +Before first build, run from repository root: +```bash +./open_source.sh +``` + +This sets up Flutter SDK (if working with Flutter Parent) and other initial configuration. + +### ProGuard +Each app has ProGuard rules in `{app}/proguard-rules.txt` + +### Private Data +The project uses `PrivateData.merge()` to inject private configuration (API keys, tokens) from `android-vault/private-data/`. These are not in version control. + +### Localization +Apps support multiple languages. Translation tags are scanned at build time via `LocaleScanner.getAvailableLanguageTags()`. \ No newline at end of file From df4a1e22c73b6f087d37929df486812311c2209d Mon Sep 17 00:00:00 2001 From: domonkosadam <53480952+domonkosadam@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:05:13 +0200 Subject: [PATCH 18/94] [CLX-2877][Horizon] Course card redesign (#3273) refs: CLX-2877 affects: Horizon release note: none --- .../canvas/espresso/mockcanvas/MockCanvas.kt | 2 + .../mockcanvas/endpoints/ApiEndpoint.kt | 2 + .../mockcanvas/endpoints/CareerEndpoints.kt | 39 ++ .../fakes/FakeGetHorizonCourseManager.kt | 114 ++++++ .../mockcanvas/fakes/FakeJourneyApiManager.kt | 74 ++++ .../canvasapi2/HorizonGetCoursesQuery.graphql | 1 + .../canvasapi2/di/graphql/GetCoursesModule.kt | 3 +- .../di/graphql/JourneyApiManagerModule.kt | 36 ++ .../managers/HorizonGetCoursesManager.kt | 72 +--- .../managers/graphql/JourneyApiManager.kt | 19 +- libs/horizon/build.gradle.kts | 26 +- .../espresso/HorizonActivityTestRule.kt | 27 ++ .../espresso/HorizonCustomTestRunner.kt | 35 ++ .../horizon/espresso/HorizonTest.kt | 43 +++ .../horizon/espresso/TestModule.kt | 301 ++++++++++++++++ .../HorizonDashboardInteractionTest.kt | 82 +++++ .../HorizonNotificationInteractionTest.kt | 64 ++++ .../horizon/pages/HorizonDashboardPage.kt | 133 +++++++ .../horizon/pages/HorizonNotificationPage.kt | 43 +++ .../HorizonDashboardCourseSectionUiTest.kt | 116 ++++++ .../notification/HorizonNotificationUiTest.kt | 209 +++++++++++ .../instructure/horizon/HorizonActivity.kt | 20 + .../features/dashboard/DashboardItemState.kt | 21 ++ .../features/dashboard/DashboardRepository.kt | 58 +-- .../features/dashboard/DashboardScreen.kt | 307 +++++----------- .../features/dashboard/DashboardUiState.kt | 41 --- .../features/dashboard/DashboardViewModel.kt | 219 +---------- .../course/DashboardCourseRepository.kt | 59 +++ .../course/DashboardCourseSection.kt | 314 ++++++++++++++++ .../course/DashboardCourseUiState.kt | 27 ++ .../course/DashboardCourseViewModel.kt | 161 +++++++++ .../dashboard/course/DashboardMapper.kt | 105 ++++++ .../course/card/DashboardCourseCard.kt | 45 +++ .../course/card/DashboardCourseCardContent.kt | 341 ++++++++++++++++++ .../course/card/DashboardCourseCardError.kt | 54 +++ .../course/card/DashboardCourseCardLoading.kt | 115 ++++++ .../course/card/DashboardCourseCardState.kt | 44 +++ .../learn/program/ProgramDetailsScreen.kt | 4 +- .../ModuleItemSequenceScreen.kt | 4 +- .../horizonui/animation/ShimmerAnimation.kt | 84 +++++ .../horizon/horizonui/molecules/Button.kt | 2 +- .../horizon/horizonui/molecules/Spinner.kt | 4 +- libs/horizon/src/main/res/values/strings.xml | 11 + .../dashboard/DashboardRepositoryTest.kt | 66 ++++ .../dashboard/DashboardViewModelTest.kt | 95 +++++ .../course/DashboardCourseRepositoryTest.kt | 184 ++++++++++ .../course/DashboardCourseViewModelTest.kt | 203 +++++++++++ .../NotificationRepositoryTest.kt | 172 +++++++++ .../notification/NotificationViewModelTest.kt | 197 ++++++++++ 49 files changed, 3802 insertions(+), 596 deletions(-) create mode 100644 automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CareerEndpoints.kt create mode 100644 automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeGetHorizonCourseManager.kt create mode 100644 automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeJourneyApiManager.kt create mode 100644 libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/JourneyApiManagerModule.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonActivityTestRule.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonCustomTestRunner.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonTest.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/TestModule.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/dashboard/HorizonDashboardInteractionTest.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/notification/HorizonNotificationInteractionTest.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonDashboardPage.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonNotificationPage.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/course/HorizonDashboardCourseSectionUiTest.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/notification/HorizonNotificationUiTest.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardItemState.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseRepository.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseSection.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseUiState.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseViewModel.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardMapper.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCard.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardContent.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardError.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardLoading.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardState.kt create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/horizonui/animation/ShimmerAnimation.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationViewModelTest.kt diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt index a13070d671..93eec07573 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt @@ -341,6 +341,8 @@ class MockCanvas { /** Sets whether dashboard_cards returns true or false for isK5Subject field. */ var elementarySubjectPages: Boolean = false + var isCareerExperience: Boolean = false + /** Returns the current auth token for the specified user. Returns null if no such token exists. */ fun tokenFor(user: User): String? { tokens.forEach { (token, userId) -> diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt index ee685b23fc..3d3121b637 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt @@ -18,6 +18,7 @@ package com.instructure.canvas.espresso.mockcanvas.endpoints import android.util.Log import com.google.gson.Gson +import com.instructure.canvas.espresso.mockCanvas.endpoints.CareerEndpoint import com.instructure.canvas.espresso.mockcanvas.Endpoint import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse import com.instructure.canvas.espresso.mockcanvas.addPlannable @@ -67,6 +68,7 @@ import java.util.Date * - `search` -> [SearchEndpoint] */ object ApiEndpoint : Endpoint( + Segment("career") to CareerEndpoint, Segment("courses") to CourseListEndpoint, Segment("users") to UserListEndpoint, Segment("accounts") to AccountListEndpoint, diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CareerEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CareerEndpoints.kt new file mode 100644 index 0000000000..28f6dd32a1 --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CareerEndpoints.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvas.espresso.mockCanvas.endpoints + +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.loginapi.login.viewmodel.Experience + +object CareerEndpoint: Endpoint( + Segment("experience_summary") to ExperienceSummaryEndpoint, +) + +object ExperienceSummaryEndpoint: Endpoint( + response = { + GET { + val experience = if (data.isCareerExperience) { + Experience.Career + } else { + Experience.Academic(data.elementarySubjectPages) + } + request.successResponse(experience) + } + } +) \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeGetHorizonCourseManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeGetHorizonCourseManager.kt new file mode 100644 index 0000000000..0c0bd4966b --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeGetHorizonCourseManager.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvas.espresso.mockcanvas.fakes + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvasapi2.GetCoursesQuery +import com.instructure.canvasapi2.managers.CourseWithModuleItemDurations +import com.instructure.canvasapi2.managers.CourseWithProgress +import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.type.EnrollmentWorkflowState +import com.instructure.canvasapi2.utils.DataResult +import java.util.Date + +class FakeGetHorizonCourseManager(): HorizonGetCoursesManager { + override suspend fun getCoursesWithProgress( + userId: Long, + forceNetwork: Boolean + ): DataResult> { + return DataResult.Success(getCourses()) + } + + override suspend fun getEnrollments( + userId: Long, + forceNetwork: Boolean + ): DataResult> { + val activeCourse = getCourses()[0] + val completedCourse = getCourses()[1] + val invitedCourse = getCourses()[2] + return DataResult.Success( + listOf( + GetCoursesQuery.Enrollment( + id = MockCanvas.data.enrollments.values.toList()[0].id.toString(), + state = EnrollmentWorkflowState.active, + lastActivityAt = Date(), + course = GetCoursesQuery.Course( + id = activeCourse.courseId.toString(), + name = activeCourse.courseName, + image_download_url = null, + syllabus_body = activeCourse.courseSyllabus, + account = GetCoursesQuery.Account( + "Account 1" + ), + usersConnection = null + ) + ), + GetCoursesQuery.Enrollment( + id = MockCanvas.data.enrollments.values.toList()[1].id.toString(), + state = EnrollmentWorkflowState.completed, + lastActivityAt = Date(), + course = GetCoursesQuery.Course( + id = completedCourse.courseId.toString(), + name = completedCourse.courseName, + image_download_url = null, + syllabus_body = completedCourse.courseSyllabus, + account = GetCoursesQuery.Account( + "Account 1" + ), + usersConnection = null + ) + ) + ) + ) + } + + override suspend fun getProgramCourses( + courseId: Long, + forceNetwork: Boolean + ): DataResult { + return DataResult.Success( + CourseWithModuleItemDurations( + courseId = courseId, + courseName = "Program Course", + ) + ) + } + + fun getCourses(): List { + val courses = MockCanvas.data.courses.values.toList() + val activeCourse = CourseWithProgress( + courseId = courses[0].id, + courseName = courses[0].name, + courseSyllabus = "Syllabus for Course 1", + progress = 0.25 + ) + val completedCourse = CourseWithProgress( + courseId = courses[1].id, + courseName = courses[1].name, + courseSyllabus = "Syllabus for Course 2", + progress = 1.0 + ) + val invitedCourse = CourseWithProgress( + courseId = courses[2].id, + courseName = courses[2].name, + courseSyllabus = null, + progress = 0.0 + ) + + return listOf(activeCourse, completedCourse, invitedCourse) + } +} \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeJourneyApiManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeJourneyApiManager.kt new file mode 100644 index 0000000000..fb072d3f94 --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeJourneyApiManager.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvas.espresso.mockcanvas.fakes + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvasapi2.managers.graphql.JourneyApiManager +import com.instructure.canvasapi2.managers.graphql.Program +import com.instructure.canvasapi2.managers.graphql.ProgramRequirement +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.journey.type.ProgramProgressCourseEnrollmentStatus +import com.instructure.journey.type.ProgramVariantType + +class FakeJourneyApiManager(): JourneyApiManager { + override suspend fun getPrograms(forceNetwork: Boolean): List { + return getPrograms() + } + + override suspend fun getProgramById(programId: String, forceNetwork: Boolean): Program { + return getPrograms().first { it.id == programId } + } + + override suspend fun enrollCourse(progressId: String): DataResult { + return if (getPrograms().first().sortedRequirements.any { it.progressId == progressId }) { + DataResult.Success(Unit) + } else { + DataResult.Fail() + } + } + + fun getPrograms(): List { + val program1 = Program( + id = "1", + name = "Program 1", + description = "Description for Program 1", + sortedRequirements = listOf( + ProgramRequirement( + id = "1", + progressId = "1", + progress = 50.0, + courseId = MockCanvas.data.courses.values.toList()[0].id, + required = true, + enrollmentStatus = ProgramProgressCourseEnrollmentStatus.ENROLLED, + ) + ), + startDate = null, + endDate = null, + variant = ProgramVariantType.LINEAR + ) + val program2 = Program( + id = "2", + name = "Program 2", + description = "Description for Program 2", + sortedRequirements = emptyList(), + startDate = null, + endDate = null, + variant = ProgramVariantType.NON_LINEAR + ) + return listOf(program1, program2) + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/HorizonGetCoursesQuery.graphql b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/HorizonGetCoursesQuery.graphql index c82370bede..c766b73c1a 100644 --- a/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/HorizonGetCoursesQuery.graphql +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/HorizonGetCoursesQuery.graphql @@ -4,6 +4,7 @@ query GetCoursesQuery($id: ID!) { enrollments(currentOnly: false, horizonCourses: true) { id: _id state + lastActivityAt course { id: _id name diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/GetCoursesModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/GetCoursesModule.kt index 62bef9cb6a..dc4a7a9d98 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/GetCoursesModule.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/GetCoursesModule.kt @@ -18,6 +18,7 @@ package com.instructure.canvasapi2.di.graphql import com.apollographql.apollo.ApolloClient import com.instructure.canvasapi2.di.DefaultApolloClient import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.HorizonGetCoursesManagerImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -29,6 +30,6 @@ class GetCoursesModule { @Provides fun provideGetCoursesManager(@DefaultApolloClient apolloClient: ApolloClient): HorizonGetCoursesManager { - return HorizonGetCoursesManager(apolloClient) + return HorizonGetCoursesManagerImpl(apolloClient) } } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/JourneyApiManagerModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/JourneyApiManagerModule.kt new file mode 100644 index 0000000000..aa4516218e --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/JourneyApiManagerModule.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 - 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.canvasapi2.di.graphql + +import com.apollographql.apollo.ApolloClient +import com.instructure.canvasapi2.di.JourneyApolloClient +import com.instructure.canvasapi2.managers.graphql.JourneyApiManager +import com.instructure.canvasapi2.managers.graphql.JourneyApiManagerImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class JourneyApiManagerModule { + + @Provides + fun provideJourneyApiManager(@JourneyApolloClient apolloClient: ApolloClient): JourneyApiManager { + return JourneyApiManagerImpl(apolloClient) + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/HorizonGetCoursesManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/HorizonGetCoursesManager.kt index ba3ddf3e50..281a02a46c 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/HorizonGetCoursesManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/HorizonGetCoursesManager.kt @@ -26,9 +26,17 @@ import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.Failure import java.util.Date -class HorizonGetCoursesManager(private val apolloClient: ApolloClient) { +interface HorizonGetCoursesManager { + suspend fun getCoursesWithProgress(userId: Long, forceNetwork: Boolean = false): DataResult> - suspend fun getCoursesWithProgress(userId: Long, forceNetwork: Boolean): DataResult> { + suspend fun getEnrollments(userId: Long, forceNetwork: Boolean = false): DataResult> + + suspend fun getProgramCourses(courseId: Long, forceNetwork: Boolean = false): DataResult +} + +class HorizonGetCoursesManagerImpl(private val apolloClient: ApolloClient): HorizonGetCoursesManager { + + override suspend fun getCoursesWithProgress(userId: Long, forceNetwork: Boolean): DataResult> { return try { val query = GetCoursesQuery(userId.toString()) val result = apolloClient.enqueueQuery(query, forceNetwork).dataAssertNoErrors @@ -55,72 +63,18 @@ class HorizonGetCoursesManager(private val apolloClient: ApolloClient) { } } - suspend fun getDashboardContent(userId: Long, forceNetwork: Boolean): DataResult { + override suspend fun getEnrollments(userId: Long, forceNetwork: Boolean): DataResult> { return try { val query = GetCoursesQuery(userId.toString()) val result = apolloClient.enqueueQuery(query, forceNetwork).dataAssertNoErrors - val coursesList = result.legacyNode?.onUser?.enrollments - ?.filter { it.state == EnrollmentWorkflowState.active } - ?.mapNotNull { mapDashboardCourse(it.course) } ?: emptyList() - val invites = result.legacyNode?.onUser?.enrollments - ?.filter { it.state == EnrollmentWorkflowState.invited } - ?.mapNotNull { mapInvites(it.course, it.id) } ?: emptyList() - return DataResult.Success(DashboardContent(coursesList, invites)) + return DataResult.Success(result.legacyNode?.onUser?.enrollments.orEmpty()) } catch (e: Exception) { DataResult.Fail(Failure.Exception(e)) } } - private fun mapDashboardCourse(course: GetCoursesQuery.Course?): DashboardCourse? { - val courseWithProgress = mapCourse(course) - val institutionName = course?.account?.name - val incompleteModulesConnection = - course?.usersConnection?.nodes?.firstOrNull()?.courseProgression?.incompleteModulesConnection?.nodes?.firstOrNull() - val nextModuleId = incompleteModulesConnection?.module?.id?.toLong() - val nextModuleItemId = incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.id?.toLong() - - val nextModuleTitle = incompleteModulesConnection?.module?.name - - val nextModuleItemEstimatedDuration = - incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.estimatedDuration - val nextModuleItemDueDate = - incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.content?.onAssignment?.dueAt - val nextModuleItemType = incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.content?.__typename - val nextModuleItemTitle = incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.content?.title - val isNewQuiz = - incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.content?.onAssignment?.isNewQuiz ?: false - - return if (courseWithProgress != null) { - DashboardCourse( - courseWithProgress, - institutionName, - nextModuleItemId, - nextModuleId, - nextModuleTitle, - nextModuleItemTitle, - nextModuleItemType, - nextModuleItemDueDate, - nextModuleItemEstimatedDuration, - isNewQuiz - ) - } else { - null - } - } - - private fun mapInvites(course: GetCoursesQuery.Course?, enrollmentId: String?): CourseInvite? { - val courseId = course?.id?.toLong() - val courseName = course?.name - - return if (courseId != null && courseName != null) { - CourseInvite(courseId, courseName, enrollmentId?.toLong() ?: -1L) - } else { - null - } - } - - suspend fun getProgramCourses(courseId: Long, forceNetwork: Boolean = false): DataResult { + override suspend fun getProgramCourses(courseId: Long, forceNetwork: Boolean): DataResult { var hasNextPage = true var nextCursor: String? = null val moduleItemDurations = mutableListOf() diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/JourneyApiManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/JourneyApiManager.kt index 25286a3eab..ec1293b08e 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/JourneyApiManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/JourneyApiManager.kt @@ -16,7 +16,6 @@ package com.instructure.canvasapi2.managers.graphql import com.apollographql.apollo.ApolloClient -import com.instructure.canvasapi2.di.JourneyApolloClient import com.instructure.canvasapi2.enqueueMutation import com.instructure.canvasapi2.enqueueQuery import com.instructure.canvasapi2.utils.DataResult @@ -50,10 +49,16 @@ data class ProgramRequirement( val enrollmentStatus: ProgramProgressCourseEnrollmentStatus? = null ) -class JourneyApiManager @Inject constructor( - @JourneyApolloClient private val journeyClient: ApolloClient -) { - suspend fun getPrograms(forceNetwork: Boolean): List { +interface JourneyApiManager { + suspend fun getPrograms(forceNetwork: Boolean = false): List + suspend fun getProgramById(programId: String, forceNetwork: Boolean = false): Program + suspend fun enrollCourse(progressId: String): DataResult +} + +class JourneyApiManagerImpl @Inject constructor( + private val journeyClient: ApolloClient +): JourneyApiManager { + override suspend fun getPrograms(forceNetwork: Boolean): List { val query = EnrolledProgramsQuery() val result = journeyClient.enqueueQuery(query, forceNetwork = forceNetwork) return result.dataAssertNoErrors.enrolledPrograms.map { @@ -61,7 +66,7 @@ class JourneyApiManager @Inject constructor( } } - suspend fun getProgramById(programId: String, forceNetwork: Boolean): Program { + override suspend fun getProgramById(programId: String, forceNetwork: Boolean): Program { val query = GetProgramByIdQuery(programId) val result = journeyClient.enqueueQuery(query, forceNetwork = forceNetwork) return mapEnrolledProgram(result.dataAssertNoErrors.program.programFields) @@ -126,7 +131,7 @@ class JourneyApiManager @Inject constructor( ) } - suspend fun enrollCourse(progressId: String): DataResult { + override suspend fun enrollCourse(progressId: String): DataResult { val mutation = EnrollCourseMutation(progressId) val result = journeyClient.enqueueMutation(mutation) return if (result.exception != null) { diff --git a/libs/horizon/build.gradle.kts b/libs/horizon/build.gradle.kts index 6b68c1b589..6da1ed1098 100644 --- a/libs/horizon/build.gradle.kts +++ b/libs/horizon/build.gradle.kts @@ -17,7 +17,7 @@ android { minSdk = Versions.MIN_SDK targetSdk = Versions.TARGET_SDK - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = "com.instructure.horizon.espresso.HorizonCustomTestRunner" consumerProguardFiles("consumer-rules.pro") } @@ -57,6 +57,10 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } + + hilt { + enableAggregatingTask = false + } } dependencies { @@ -85,4 +89,24 @@ dependencies { implementation(Libs.FIREBASE_CRASHLYTICS) { isTransitive = true } + + /* Android Test Dependencies */ + androidTestImplementation(project(":espresso")) + androidTestImplementation(project(":dataseedingapi")) + androidTestImplementation(Libs.COMPOSE_UI_TEST) + + /* Unit Test Dependencies */ + testImplementation(Libs.JUNIT) + testImplementation(Libs.ROBOLECTRIC) + testImplementation(Libs.ANDROIDX_TEST_JUNIT) + testImplementation(Libs.MOCKK) + androidTestImplementation(Libs.ANDROIDX_TEST_JUNIT) + testImplementation(Libs.KOTLIN_COROUTINES_TEST) + testImplementation(Libs.THREETEN_BP) + testImplementation(Libs.ANDROIDX_CORE_TESTING) + androidTestImplementation(Libs.HILT_TESTING) + + /* Pandautils dependencies to provide fake implementations for testing */ + androidTestImplementation(Libs.PLAY_IN_APP_UPDATES) + androidTestImplementation(Libs.ROOM) } \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonActivityTestRule.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonActivityTestRule.kt new file mode 100644 index 0000000000..c0e78bd6d4 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonActivityTestRule.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 - 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.horizon.espresso + +import android.app.Activity +import android.content.Context +import com.instructure.espresso.InstructureActivityTestRule + +class HorizonActivityTestRule(activityClass: Class) : InstructureActivityTestRule(activityClass) { + override fun performReset(context: Context) { + + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonCustomTestRunner.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonCustomTestRunner.kt new file mode 100644 index 0000000000..4ed54cefce --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonCustomTestRunner.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 - 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.horizon.espresso + +import android.app.Application +import android.content.Context +import com.instructure.canvas.espresso.CanvasRunner +import com.jakewharton.threetenabp.AndroidThreeTen +import dagger.hilt.android.testing.HiltTestApplication + +class HorizonCustomTestRunner: CanvasRunner() { + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context? + ): Application { + val application = super.newApplication(cl, HiltTestApplication::class.java.name, context) + AndroidThreeTen.init(application) + return application + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonTest.kt new file mode 100644 index 0000000000..b5570bdbdd --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 - 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.horizon.espresso + +import com.instructure.canvas.espresso.CanvasComposeTest +import com.instructure.canvasapi2.models.User +import com.instructure.horizon.HorizonActivity +import com.instructure.horizon.pages.HorizonDashboardPage +import com.instructure.horizon.pages.HorizonNotificationPage + +abstract class HorizonTest: CanvasComposeTest() { + override val activityRule = HorizonActivityTestRule(HorizonActivity::class.java) + override val isTesting = true + + override fun displaysPageObjects() = Unit + + val dashboardPage: HorizonDashboardPage = HorizonDashboardPage(composeTestRule) + val notificationsPage: HorizonNotificationPage = HorizonNotificationPage(composeTestRule) + + fun tokenLogin(domain: String, token: String, user: User) { + activityRule.runOnUiThread { + (originalActivity as HorizonActivity).loginWithToken( + token, + domain, + user + ) + } + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/TestModule.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/TestModule.kt new file mode 100644 index 0000000000..9997f8b0a6 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/TestModule.kt @@ -0,0 +1,301 @@ +package com.instructure.horizon.espresso + +import android.content.Intent +import com.instructure.canvasapi2.LoginRouter +import com.instructure.pandautils.features.offline.sync.SyncRouter +import com.instructure.pandautils.features.speedgrader.content.SpeedGraderContentRouter +import com.instructure.pandautils.features.speedgrader.grade.comments.SpeedGraderCommentsAttachmentRouter +import com.instructure.pandautils.receivers.alarm.AlarmReceiverNotificationHandler +import com.instructure.pandautils.room.appdatabase.AppDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object HorizonTestModule { + @Provides + fun provideSpeedGraderContentRouter(): SpeedGraderContentRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSpeedGraderCommentsAttachmentRouter(): SpeedGraderCommentsAttachmentRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAlarmReceiverNotificationHandler(): AlarmReceiverNotificationHandler { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideLoginRouter(): LoginRouter { + return object : LoginRouter { + override fun loginIntent(): Intent { + return Intent() + } + } + } + + @Provides + fun provideAppDatabase(): AppDatabase { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSyncRouter(): SyncRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideLogoutHelper(): com.instructure.pandautils.utils.LogoutHelper { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun providePandataInfoAppKey(): com.instructure.canvasapi2.utils.pageview.PandataInfo.AppKey { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideDiscussionRouteHelperRepository(): com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentDetailsRouter(): com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideWebViewRouter(): com.instructure.pandautils.navigation.WebViewRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentDetailsBehaviour(): com.instructure.pandautils.features.assignments.details.AssignmentDetailsBehaviour { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentListRouter(): com.instructure.pandautils.features.assignments.list.AssignmentListRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCalendarRouter(): com.instructure.pandautils.features.calendar.CalendarRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideEventRouter(): com.instructure.pandautils.features.calendarevent.details.EventRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideToDoRouter(): com.instructure.pandautils.features.calendartodo.details.ToDoRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideEditDashboardRouter(): com.instructure.pandautils.features.dashboard.edit.EditDashboardRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideShareExtensionRouter(): com.instructure.pandautils.features.shareextension.ShareExtensionRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideDashboardRouter(): com.instructure.pandautils.features.dashboard.notifications.DashboardRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideDiscussionRouter(): com.instructure.pandautils.features.discussion.router.DiscussionRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideDiscussionDetailsWebViewFragmentBehavior(): com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragmentBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideGradesRouter(): com.instructure.pandautils.features.elementary.grades.GradesRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideHomeroomRouter(): com.instructure.pandautils.features.elementary.homeroom.HomeroomRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideImportantDatesRouter(): com.instructure.pandautils.features.elementary.importantdates.ImportantDatesRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideResourcesRouter(): com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideScheduleRouter(): com.instructure.pandautils.features.elementary.schedule.ScheduleRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideHelpDialogFragmentBehavior(): com.instructure.pandautils.features.help.HelpDialogFragmentBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideInboxRouter(): com.instructure.pandautils.features.inbox.list.InboxRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideLegalRouter(): com.instructure.pandautils.features.legal.LegalRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideLtiLaunchFragmentBehavior(): com.instructure.pandautils.features.lti.LtiLaunchFragmentBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSettingsRouter(): com.instructure.pandautils.features.settings.SettingsRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSmartSearchRouter(): com.instructure.pandautils.features.smartsearch.SmartSearchRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAboutRepository(): com.instructure.pandautils.features.about.AboutRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentDetailsRepository(): com.instructure.pandautils.features.assignments.details.AssignmentDetailsRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentDetailsSubmissionHandler(): com.instructure.pandautils.features.assignments.details.AssignmentDetailsSubmissionHandler { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentDetailsColorProvider(): com.instructure.pandautils.features.assignments.details.AssignmentDetailsColorProvider { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentListRepository(): com.instructure.pandautils.features.assignments.list.AssignmentListRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentListBehavior(): com.instructure.pandautils.features.assignments.list.AssignmentListBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCalendarRepository(): com.instructure.pandautils.features.calendar.CalendarRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCalendarBehavior(): com.instructure.pandautils.features.calendar.CalendarBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCreateUpdateEventRepository(): com.instructure.pandautils.features.calendarevent.createupdate.CreateUpdateEventRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCreateUpdateEventViewModelBehavior(): com.instructure.pandautils.features.calendarevent.createupdate.CreateUpdateEventViewModelBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCreateUpdateToDoRepository(): com.instructure.pandautils.features.calendartodo.createupdate.CreateUpdateToDoRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCreateUpdateToDoViewModelBehavior(): com.instructure.pandautils.features.calendartodo.createupdate.CreateUpdateToDoViewModelBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideEditDashboardRepository(): com.instructure.pandautils.features.dashboard.edit.EditDashboardRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideEventViewModelBehavior(): com.instructure.pandautils.features.calendarevent.details.EventViewModelBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideGradesBehaviour(): com.instructure.pandautils.features.grades.GradesBehaviour { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideGradesRepository(): com.instructure.pandautils.features.grades.GradesRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideHelpLinkFilter(): com.instructure.pandautils.features.help.HelpLinkFilter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideInboxComposeRepository(): com.instructure.pandautils.features.inbox.compose.InboxComposeRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideInboxComposeBehavior(): com.instructure.pandautils.features.inbox.compose.InboxComposeBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideInboxDetailsBehavior(): com.instructure.pandautils.features.inbox.details.InboxDetailsBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideInboxRepository(): com.instructure.pandautils.features.inbox.list.InboxRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSettingsBehaviour(): com.instructure.pandautils.features.settings.SettingsBehaviour { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSpeedGraderPostPolicyRouter(): com.instructure.pandautils.features.speedgrader.SpeedGraderPostPolicyRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideToDoViewModelBehavior(): com.instructure.pandautils.features.calendartodo.details.ToDoViewModelBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/dashboard/HorizonDashboardInteractionTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/dashboard/HorizonDashboardInteractionTest.kt new file mode 100644 index 0000000000..52468f216b --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/dashboard/HorizonDashboardInteractionTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2025 - 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.horizon.interaction.features.dashboard + +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetHorizonCourseManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeJourneyApiManager +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addItemToModule +import com.instructure.canvas.espresso.mockcanvas.addModuleToCourse +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvasapi2.di.graphql.GetCoursesModule +import com.instructure.canvasapi2.di.graphql.JourneyApiManagerModule +import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.JourneyApiManager +import com.instructure.canvasapi2.models.Page +import com.instructure.horizon.espresso.HorizonTest +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import org.junit.Test + +@HiltAndroidTest +@UninstallModules(GetCoursesModule::class, JourneyApiManagerModule::class) +class HorizonDashboardInteractionTest: HorizonTest() { + private val fakeGetHorizonCourseManager = FakeGetHorizonCourseManager() + private val fakeJourneyApiManager = FakeJourneyApiManager() + + @BindValue + @JvmField + val journeyApiManager: JourneyApiManager = fakeJourneyApiManager + + @BindValue + @JvmField + val getCoursesManager: HorizonGetCoursesManager = fakeGetHorizonCourseManager + + @Test + fun testDashboardCards() { + val data = MockCanvas.init( + studentCount = 1, + teacherCount = 1, + courseCount = 3 + ) + val course1 = data.courses.values.toList()[0] + val course2 = data.courses.values.toList()[1] + val module1 = data.addModuleToCourse(course1, "Module 0") + val module2 = data.addModuleToCourse(course2, "Module 1") + val moduleItem1 = data.addItemToModule(course1, module1.id, Page(title = "Module Item 1")) + val moduleItem2 = data.addItemToModule(course2, module2.id, Page(title = "Module Item 2")) + val student = data.students.first() + val token = data.tokenFor(student)!! + tokenLogin(data.domain, token, student) + + dashboardPage.assertNotStartedProgramDisplayed(fakeJourneyApiManager.getPrograms()[1].name) + dashboardPage.assertCourseCardDisplayed( + course1.name, + listOf(fakeJourneyApiManager.getPrograms()[0].name), + fakeGetHorizonCourseManager.getCourses().first().progress, + moduleItem1.title + ) + + dashboardPage.selectCourseCardAtIndex(1, 2) + + dashboardPage.assertCourseCardDisplayed( + course2.name, + progress = fakeGetHorizonCourseManager.getCourses()[1].progress, + ) + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/notification/HorizonNotificationInteractionTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/notification/HorizonNotificationInteractionTest.kt new file mode 100644 index 0000000000..8d9420288c --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/notification/HorizonNotificationInteractionTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2025 - 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.horizon.interaction.features.notification + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAccountNotification +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetHorizonCourseManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeJourneyApiManager +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvasapi2.di.graphql.GetCoursesModule +import com.instructure.canvasapi2.di.graphql.JourneyApiManagerModule +import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.JourneyApiManager +import com.instructure.horizon.espresso.HorizonTest +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import org.junit.Test + +@HiltAndroidTest +@UninstallModules(GetCoursesModule::class, JourneyApiManagerModule::class) +class HorizonNotificationInteractionTest: HorizonTest() { + private val fakeGetHorizonCourseManager = FakeGetHorizonCourseManager() + private val fakeJourneyApiManager = FakeJourneyApiManager() + + @BindValue + @JvmField + val journeyApiManager: JourneyApiManager = fakeJourneyApiManager + + @BindValue + @JvmField + val getCoursesManager: HorizonGetCoursesManager = fakeGetHorizonCourseManager + + @Test + fun testNotifications() { + val data = MockCanvas.init( + studentCount = 1, + teacherCount = 1, + courseCount = 3 + ) + val student = data.students.first() + val token = data.tokenFor(student)!! + tokenLogin(data.domain, token, student) + + val accountNotification = data.addAccountNotification() + dashboardPage.clickNotificationButton() + notificationsPage.assertNotificationItem(accountNotification.subject, "Announcement") + + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonDashboardPage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonDashboardPage.kt new file mode 100644 index 0000000000..94e5f882a0 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonDashboardPage.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2025 - 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.horizon.pages + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onParent +import androidx.compose.ui.test.performClick +import kotlin.math.roundToInt + +class HorizonDashboardPage(private val composeTestRule: ComposeTestRule) { + fun assertNotStartedProgramDisplayed(programName: String) { + composeTestRule.onNodeWithText(programName) + .assertIsDisplayed() + + composeTestRule.onNodeWithText(programName) + .onParent() + .onChildren() + .filterToOne(hasText("Welcome! View your program to enroll in your first course.")) + .assertIsDisplayed() + + composeTestRule.onNodeWithText(programName) + .onParent() + .onChildren() + .filterToOne(hasAnyDescendant(hasText("Program details"))) + .assertIsDisplayed() + .assertHasClickAction() + } + + fun clickProgramDetails(programName: String) { + composeTestRule.onNodeWithText(programName) + .onParent() + .onChildren() + .filterToOne(hasAnyDescendant(hasText("Program details"))) + .assertIsDisplayed() + .performClick() + } + + fun assertCourseCardDisplayed( + courseName: String, + programNames: List = emptyList(), + progress: Double? = null, + moduleItemName: String? = null + ) { + val courseCardParent = composeTestRule.onNodeWithText(courseName) + .assertIsDisplayed() + .assertHasClickAction() + .onParent() + .onParent() + + courseCardParent.onChildren() + .filterToOne(hasAnyDescendant(hasText(courseName))) + .assertIsDisplayed() + + programNames.forEach { programName -> + courseCardParent.onChildren() + .filterToOne(hasAnyDescendant(hasText(programName, substring = true))) + .assertIsDisplayed() + } + + if (progress != null) { + courseCardParent.onChildren() + .filterToOne(hasAnyDescendant(hasText(progress.roundToInt().toString(), substring = true))) + .assertIsDisplayed() + } + + if (moduleItemName != null) { + courseCardParent.onChildren() + .filterToOne(hasAnyDescendant(hasText(moduleItemName))) + .onChildren().onFirst() + .assertIsDisplayed() + .assertHasClickAction() + } + } + + fun clickCourseCard(courseName: String) { + composeTestRule.onNodeWithText(courseName) + .performClick() + } + + fun clickCourseCardModuleItem(courseName: String, moduleItemName: String) { + composeTestRule.onNodeWithText(courseName) + .assertIsDisplayed() + .assertHasClickAction() + .onParent() + .onParent() + .onChildren() + .filterToOne(hasAnyDescendant(hasText(moduleItemName))) + .onChildren().onFirst() + .assertIsDisplayed() + .performClick() + } + + fun selectCourseCardAtIndex(index: Int, courseCount: Int) { + composeTestRule.onNodeWithContentDescription( + "Course Card ${index + 1} of ${courseCount}" + ).performClick() + } + + fun clickInboxButton() { + composeTestRule.onNodeWithContentDescription("Inbox").performClick() + } + + fun clickNotificationButton() { + composeTestRule.onNodeWithContentDescription("Notifications").performClick() + } + + fun clickNotebookButton() { + composeTestRule.onNodeWithContentDescription("Notebook").performClick() + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonNotificationPage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonNotificationPage.kt new file mode 100644 index 0000000000..772bf5259b --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonNotificationPage.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 - 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.horizon.pages + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyChild +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onChild +import androidx.compose.ui.test.performClick + +class HorizonNotificationPage(private val composeTestRule: ComposeTestRule) { + fun assertNotificationItem(title: String, label: String) { + composeTestRule.onNode( + hasAnyChild(hasText(label)).and( + hasAnyChild(hasText(title)) + ) + ).assertIsDisplayed().onChild().assertHasClickAction() + } + + fun clickNotificationItem(title: String, label: String) { + composeTestRule.onNode( + hasAnyChild(hasText(label)).and( + hasAnyChild(hasText(title)) + ) + ).onChild().performClick() + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/course/HorizonDashboardCourseSectionUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/course/HorizonDashboardCourseSectionUiTest.kt new file mode 100644 index 0000000000..e8660f2ef4 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/course/HorizonDashboardCourseSectionUiTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2025 - 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.horizon.ui.features.dashboard.course + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performScrollTo +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.course.DashboardCourseSection +import com.instructure.horizon.features.dashboard.course.DashboardCourseUiState +import com.instructure.horizon.features.dashboard.course.card.CardClickAction +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardButtonState +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardModuleItemState +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardParentProgramState +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardState +import com.instructure.horizon.model.LearningObjectType +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class HorizonDashboardCourseSectionUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testProgramAndCourseCards() { + val state = DashboardCourseUiState( + state = DashboardItemState.SUCCESS, + programs = listOf( + DashboardCourseCardState( + title = "Program 1", + description = "Welcome!", + buttonState = DashboardCourseCardButtonState( + label = "Program details", + onClickAction = CardClickAction.Action {} + ) + ) + ), + courses = listOf( + DashboardCourseCardState( + parentPrograms = listOf( + DashboardCourseCardParentProgramState( + programName = "Program 11", + programId = "1", + onClickAction = CardClickAction.Action {} + ), + DashboardCourseCardParentProgramState( + programName = "Program 12", + programId = "2", + onClickAction = CardClickAction.Action {} + ) + ), + title = "Course 1", + moduleItem = DashboardCourseCardModuleItemState( + moduleItemTitle = "Module Item 1", + moduleItemType = LearningObjectType.PAGE, + dueDate = Date(), + estimatedDuration = "5 min", + onClickAction = CardClickAction.Action {} + ) + ), + DashboardCourseCardState( + title = "Course 2", + moduleItem = DashboardCourseCardModuleItemState( + moduleItemTitle = "Module Item 2", + moduleItemType = LearningObjectType.ASSIGNMENT, + dueDate = Date(), + estimatedDuration = "10 min", + onClickAction = CardClickAction.Action {} + ) + ) + ) + ) + composeTestRule.setContent { + val mainNavController = rememberNavController() + val homeNavController = rememberNavController() + DashboardCourseSection(state, mainNavController,homeNavController) + } + + composeTestRule.onNodeWithText("Program 1").assertExists() + composeTestRule.onNodeWithText("Welcome!").assertExists() + composeTestRule.onNodeWithText("Program details").assertExists().assertHasClickAction() + + composeTestRule.onNodeWithText("Course 1").performScrollTo().assertExists() + composeTestRule.onNodeWithText("Program 11", true).assertExists().assertHasClickAction() + composeTestRule.onNodeWithText("Program 12", true).assertExists().assertHasClickAction() + composeTestRule.onNodeWithText("Module Item 1").assertExists().assertHasClickAction() + composeTestRule.onNodeWithText("5 min").assertExists().assertHasClickAction() + composeTestRule.onNodeWithText("Page").assertExists().assertHasClickAction() + + composeTestRule.onNodeWithText("Course 2").performScrollTo().assertExists() + composeTestRule.onNodeWithText("Module Item 2").assertExists().assertHasClickAction() + composeTestRule.onNodeWithText("10 min").assertExists().assertHasClickAction() + composeTestRule.onNodeWithText("Assignment").assertExists().assertHasClickAction() + } + +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/notification/HorizonNotificationUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/notification/HorizonNotificationUiTest.kt new file mode 100644 index 0000000000..0f197d3bff --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/notification/HorizonNotificationUiTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2025 - 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.horizon.ui.features.notification + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.features.notification.NotificationItem +import com.instructure.horizon.features.notification.NotificationItemCategory +import com.instructure.horizon.features.notification.NotificationRoute +import com.instructure.horizon.features.notification.NotificationScreen +import com.instructure.horizon.features.notification.NotificationUiState +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import com.instructure.horizon.horizonui.platform.LoadingState +import com.instructure.pandautils.utils.localisedFormat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +@RunWith(AndroidJUnit4::class) +class HorizonNotificationUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testLoadingState() { + val state = NotificationUiState( + screenState = LoadingState(isLoading = true), + ) + composeTestRule.setContent { + NotificationScreen(state, rememberNavController()) + } + + composeTestRule.onNodeWithText("Notifications") + .assertIsDisplayed() + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("Navigate back") + .assertIsDisplayed() + .assertHasClickAction() + } + + @Test + fun testContentStateTodayFormat() { + val state = NotificationUiState( + screenState = LoadingState(), + notificationItems = listOf( + NotificationItem( + category = NotificationItemCategory("Announcement", StatusChipColor.Sky), + courseLabel = "Biology 101", + title = "New Announcement", + date = Date(), + isRead = false, + route = NotificationRoute.DeepLink("") + ) + ) + ) + composeTestRule.setContent { + NotificationScreen(state, rememberNavController()) + } + + composeTestRule.onNodeWithText("Notifications") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("Navigate back") + .assertIsDisplayed() + .assertHasClickAction() + + composeTestRule.onNodeWithText("Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("New Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Biology 101") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Today") + .assertIsDisplayed() + } + + @Test + fun testContentStateYesterdayFormat() { + val state = NotificationUiState( + screenState = LoadingState(), + notificationItems = listOf( + NotificationItem( + category = NotificationItemCategory("Announcement", StatusChipColor.Sky), + courseLabel = "Biology 101", + title = "New Announcement", + date = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) }.time, + isRead = false, + route = NotificationRoute.DeepLink("") + ) + ) + ) + composeTestRule.setContent { + NotificationScreen(state, rememberNavController()) + } + + composeTestRule.onNodeWithText("Notifications") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("Navigate back") + .assertIsDisplayed() + .assertHasClickAction() + + composeTestRule.onNodeWithText("Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("New Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Biology 101") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Yesterday") + .assertIsDisplayed() + } + + @Test + fun testContentStateDayOfWeekFormat() { + val date = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -3) }.time + val state = NotificationUiState( + screenState = LoadingState(), + notificationItems = listOf( + NotificationItem( + category = NotificationItemCategory("Announcement", StatusChipColor.Sky), + courseLabel = "Biology 101", + title = "New Announcement", + date = date, + isRead = false, + route = NotificationRoute.DeepLink("") + ) + ) + ) + composeTestRule.setContent { + NotificationScreen(state, rememberNavController()) + } + val dateString = SimpleDateFormat("EEEE", Locale.getDefault()).format(date) + + composeTestRule.onNodeWithText("Notifications") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("Navigate back") + .assertIsDisplayed() + .assertHasClickAction() + + composeTestRule.onNodeWithText("Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("New Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Biology 101") + .assertIsDisplayed() + composeTestRule.onNodeWithText(dateString) + .assertIsDisplayed() + } + + @Test + fun testContentStateDateFormat() { + val date = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -7) }.time + val state = NotificationUiState( + screenState = LoadingState(), + notificationItems = listOf( + NotificationItem( + category = NotificationItemCategory("Announcement", StatusChipColor.Sky), + courseLabel = "Biology 101", + title = "New Announcement", + date = date, + isRead = false, + route = NotificationRoute.DeepLink("") + ) + ) + ) + composeTestRule.setContent { + NotificationScreen(state, rememberNavController()) + } + val dateString = date.localisedFormat("MMM dd, yyyy") + + composeTestRule.onNodeWithText("Notifications") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("Navigate back") + .assertIsDisplayed() + .assertHasClickAction() + + composeTestRule.onNodeWithText("Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("New Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Biology 101") + .assertIsDisplayed() + composeTestRule.onNodeWithText(dateString) + .assertIsDisplayed() + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt b/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt index 0870bd006f..9d8dbf869c 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt @@ -34,6 +34,8 @@ import androidx.navigation.NavDeepLinkRequest import androidx.navigation.NavHostController import androidx.navigation.NavOptions import androidx.navigation.compose.rememberNavController +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.horizon.horizonui.HorizonTheme import com.instructure.horizon.navigation.HorizonNavigation import com.instructure.pandautils.base.BaseCanvasActivity @@ -43,6 +45,7 @@ import com.instructure.pandautils.utils.AppTheme import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.Utils import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.WebViewAuthenticator import com.instructure.pandautils.utils.getActivityOrNull @@ -151,4 +154,21 @@ class HorizonActivity : BaseCanvasActivity() { } return flag } + + /** + * ONLY USE FOR UI TESTING + * Skips the traditional login process by directly setting the domain, token, and user info. + */ + fun loginWithToken(token: String, domain: String, user: User) { + ApiPrefs.accessToken = token + ApiPrefs.domain = domain + ApiPrefs.user = user + ApiPrefs.canvasCareerView = true + ApiPrefs.userAgent = Utils.generateUserAgent(this, Const.STUDENT_USER_AGENT) + finish() + val intent = Intent(this, HorizonActivity::class.java).apply { + intent?.extras?.let { putExtras(it) } + } + startActivity(intent) + } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardItemState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardItemState.kt new file mode 100644 index 0000000000..76a0f49982 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardItemState.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard + +enum class DashboardItemState { + LOADING, ERROR, SUCCESS +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt index 172216885a..74c5f09f30 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt @@ -1,66 +1,30 @@ /* * Copyright (C) 2025 - 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 . * - * 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.horizon.features.dashboard -import com.instructure.canvasapi2.apis.EnrollmentAPI -import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.apis.UnreadCountAPI import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.managers.DashboardContent -import com.instructure.canvasapi2.managers.HorizonGetCoursesManager -import com.instructure.canvasapi2.managers.graphql.JourneyApiManager -import com.instructure.canvasapi2.managers.graphql.Program -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.models.UnreadNotificationCount -import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.canvasapi2.utils.DataResult import javax.inject.Inject class DashboardRepository @Inject constructor( - private val horizonGetCoursesManager: HorizonGetCoursesManager, - private val moduleApi: ModuleAPI.ModuleInterface, - private val apiPrefs: ApiPrefs, - private val enrollmentApi: EnrollmentAPI.EnrollmentInterface, - private val journeyApiManager: JourneyApiManager, private val unreadCountApi: UnreadCountAPI.UnreadCountsInterface, ) { - suspend fun getDashboardContent(forceNetwork: Boolean): DataResult { - return horizonGetCoursesManager.getDashboardContent(apiPrefs.user?.id ?: -1, forceNetwork) - } - - suspend fun getFirstPageModulesWithItems(courseId: Long, forceNetwork: Boolean): DataResult> { - val params = RestParams(isForceReadFromNetwork = forceNetwork) - return moduleApi.getFirstPageModulesWithItems( - CanvasContext.Type.COURSE.apiString, - courseId, - params, - includes = listOf("estimated_durations") - ) - } - - suspend fun acceptInvite(courseId: Long, enrollmentId: Long) { - return enrollmentApi.acceptInvite(courseId, enrollmentId, RestParams()).dataOrThrow - } - - suspend fun getPrograms(forceNetwork: Boolean = false): List { - return journeyApiManager.getPrograms(forceNetwork) - } - - suspend fun getUnreadCounts(forceNetwork: Boolean = true): List { + suspend fun getUnreadCounts(forceNetwork: Boolean): List { return unreadCountApi.getNotificationsCount(RestParams(isForceReadFromNetwork = forceNetwork)).dataOrNull.orEmpty() } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt index 5401bd651f..2caf5f1e2e 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt @@ -22,25 +22,20 @@ import android.content.pm.PackageManager import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -51,160 +46,121 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.LinkAnnotation -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLinkStyles -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withLink import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.navigation.NavController -import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R -import com.instructure.horizon.features.home.HomeNavigationRoute -import com.instructure.horizon.features.moduleitemsequence.SHOULD_REFRESH_DASHBOARD +import com.instructure.horizon.features.dashboard.course.DashboardCourseSection +import com.instructure.horizon.horizonui.animation.shimmerEffect import com.instructure.horizon.horizonui.foundation.HorizonColors -import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius import com.instructure.horizon.horizonui.foundation.HorizonElevation import com.instructure.horizon.horizonui.foundation.HorizonSpace -import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.foundation.SpaceSize import com.instructure.horizon.horizonui.molecules.Badge import com.instructure.horizon.horizonui.molecules.BadgeContent import com.instructure.horizon.horizonui.molecules.BadgeType -import com.instructure.horizon.horizonui.molecules.Button -import com.instructure.horizon.horizonui.molecules.ButtonColor import com.instructure.horizon.horizonui.molecules.IconButton import com.instructure.horizon.horizonui.molecules.IconButtonColor -import com.instructure.horizon.horizonui.molecules.LoadingButton -import com.instructure.horizon.horizonui.molecules.ProgressBar -import com.instructure.horizon.horizonui.organisms.Alert -import com.instructure.horizon.horizonui.organisms.AlertType -import com.instructure.horizon.horizonui.organisms.cards.LearningObjectCard -import com.instructure.horizon.horizonui.organisms.cards.LearningObjectCardState -import com.instructure.horizon.horizonui.platform.LoadingStateWrapper import com.instructure.horizon.navigation.MainNavigationRoute -import com.instructure.pandautils.utils.ThemePrefs +import kotlinx.coroutines.flow.MutableStateFlow + +const val DASHBOARD_REFRESH = "refreshDashboard" +const val DASHBOARD_SNACKBAR = "dashboardSnackbar" @Composable fun DashboardScreen(uiState: DashboardUiState, mainNavController: NavHostController, homeNavController: NavHostController) { + val snackbarHostState = remember { SnackbarHostState() } val parentEntry = remember(mainNavController.currentBackStackEntry) { mainNavController.getBackStackEntry("home") } val savedStateHandle = parentEntry.savedStateHandle - val refreshFlow = remember { savedStateHandle.getStateFlow(SHOULD_REFRESH_DASHBOARD, false) } + val externalRefreshFlow = remember { savedStateHandle.getStateFlow(DASHBOARD_REFRESH, false) } + val externalRefreshState by externalRefreshFlow.collectAsState() + var shouldRefresh by rememberSaveable { mutableStateOf(false) } + + val snackbarFlow = remember { savedStateHandle.getStateFlow(DASHBOARD_SNACKBAR, "") } + val snackbar by snackbarFlow.collectAsState() - val shouldRefresh by refreshFlow.collectAsState() + /* + Using a list of booleans to represent each refreshing component. + Components get the `shouldRefresh` flag to start refreshing on pull-to-refresh. + Each component append the `refreshStateFlow` with `true` when starting to refresh and remove it when done. + If any component is refreshing, the dashboard shows the refreshing indicator. + */ + val refreshStateFlow = remember { MutableStateFlow(emptyList()) } + val refreshState by refreshStateFlow.collectAsState() NotificationPermissionRequest() - LaunchedEffect(shouldRefresh) { - if (shouldRefresh) { - uiState.loadingState.onRefresh() - savedStateHandle[SHOULD_REFRESH_DASHBOARD] = false + LaunchedEffect(shouldRefresh, externalRefreshState) { + if (shouldRefresh || externalRefreshState) { + savedStateHandle[DASHBOARD_REFRESH] = false + shouldRefresh = false } } - Scaffold(containerColor = HorizonColors.Surface.pagePrimary()) { paddingValues -> - val spinnerColor = - if (ThemePrefs.isThemeApplied) HorizonColors.Surface.institution() else HorizonColors.Surface.inverseSecondary() - LoadingStateWrapper(loadingState = uiState.loadingState, spinnerColor = spinnerColor, modifier = Modifier.padding(paddingValues)) { - if (uiState.coursesUiState.isEmpty() && uiState.invitesUiState.isEmpty()) { - Column( + LaunchedEffect(snackbar) { + if (snackbar.isNotEmpty()) { + snackbarHostState.showSnackbar( + message = snackbar, + ) + savedStateHandle[DASHBOARD_SNACKBAR] = "" + } + } + + Scaffold( + containerColor = HorizonColors.Surface.pagePrimary(), + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { paddingValues -> + val pullToRefreshState = rememberPullToRefreshState() + val isRefreshing = refreshState.any { it } + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { shouldRefresh = true }, + state = pullToRefreshState, + indicator = { + Indicator( modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp) - .verticalScroll(rememberScrollState()) - .height(IntrinsicSize.Max) - ) { - HomeScreenTopBar(uiState, mainNavController, modifier = Modifier.height(56.dp)) - HorizonSpace(SpaceSize.SPACE_24) - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text( - stringResource(R.string.dashboard_emptyMessage), - style = HorizonTypography.h3, - textAlign = TextAlign.Center + .align(Alignment.TopCenter) + .padding(top = 16.dp), + isRefreshing = isRefreshing, + containerColor = HorizonColors.Surface.pageSecondary(), + color = HorizonColors.Surface.institution(), + state = pullToRefreshState + ) + } + ){ + LazyColumn( + modifier = Modifier + .padding(paddingValues) + ) { + item { + HomeScreenTopBar( + uiState, + mainNavController, + modifier = Modifier.height(56.dp) + ) + } + item { + Column { + HorizonSpace(SpaceSize.SPACE_24) + DashboardCourseSection( + mainNavController, + homeNavController, + shouldRefresh, + refreshStateFlow ) } } - } else { - LazyColumn( - contentPadding = PaddingValues(start = 24.dp, end = 24.dp), - content = { - item { - HomeScreenTopBar(uiState, mainNavController, modifier = Modifier.height(56.dp)) - HorizonSpace(SpaceSize.SPACE_24) - } - items(uiState.invitesUiState) { inviteItem -> - Alert( - stringResource(R.string.dashboard_courseInvite, inviteItem.courseName), - alertType = AlertType.Info, - buttons = { - LoadingButton( - label = stringResource(R.string.dashboard_courseInviteAccept), - contentAlignment = Alignment.CenterStart, - color = ButtonColor.Black, - onClick = inviteItem.onAccept, - loading = inviteItem.acceptLoading - ) - }, - onDismiss = if (inviteItem.acceptLoading) null else inviteItem.onDismiss - ) - HorizonSpace(SpaceSize.SPACE_16) - } - items(uiState.programsUiState) { program -> - DashboardProgramItem(program) { - homeNavController.navigate(HomeNavigationRoute.Learn.withProgram(program.id)) { - popUpTo(homeNavController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = false - } - } - } - itemsIndexed(uiState.coursesUiState) { index, courseItem -> - DashboardCourseItem(courseItem, onClick = { - mainNavController.navigate( - MainNavigationRoute.ModuleItemSequence( - courseItem.courseId, - courseItem.nextModuleItemId - ) - ) - }, onCourseClick = { - homeNavController.navigate(HomeNavigationRoute.Learn.withCourse(courseItem.courseId)) { - popUpTo(homeNavController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = false - } - }, onProgramClick = { programId -> - homeNavController.navigate(HomeNavigationRoute.Learn.withProgram(programId)) { - popUpTo(homeNavController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = false - } - }) - if (index < uiState.coursesUiState.size - 1) { - HorizonSpace(SpaceSize.SPACE_48) - } - } - }) } } } @@ -212,18 +168,20 @@ fun DashboardScreen(uiState: DashboardUiState, mainNavController: NavHostControl @Composable private fun HomeScreenTopBar(uiState: DashboardUiState, mainNavController: NavController, modifier: Modifier = Modifier) { - Row(verticalAlignment = Alignment.Bottom, modifier = modifier) { + Row(verticalAlignment = Alignment.Bottom, modifier = modifier.padding(horizontal = 24.dp)) { GlideImage( model = uiState.logoUrl, contentScale = ContentScale.Fit, modifier = Modifier .weight(1f) - .heightIn(max = 44.dp), + .heightIn(max = 44.dp) + .shimmerEffect(uiState.logoUrl.isEmpty()), contentDescription = stringResource(R.string.a11y_institutionLogoContentDescription), ) Spacer(modifier = Modifier.weight(1f)) IconButton( iconRes = R.drawable.menu_book_notebook, + contentDescription = stringResource(R.string.a11y_dashboardNotebookButtonContentDescription), onClick = { mainNavController.navigate(MainNavigationRoute.Notebook.route) }, @@ -233,6 +191,7 @@ private fun HomeScreenTopBar(uiState: DashboardUiState, mainNavController: NavCo HorizonSpace(SpaceSize.SPACE_8) IconButton( iconRes = R.drawable.notifications, + contentDescription = stringResource(R.string.a11y_dashboardNotificationsContentDescription), onClick = { mainNavController.navigate(MainNavigationRoute.Notification.route) }, @@ -250,6 +209,7 @@ private fun HomeScreenTopBar(uiState: DashboardUiState, mainNavController: NavCo HorizonSpace(SpaceSize.SPACE_8) IconButton( iconRes = R.drawable.mail, + contentDescription = stringResource(R.string.a11y_dashboardInboxContentDescription), onClick = { mainNavController.navigate(MainNavigationRoute.Inbox.route) }, elevation = HorizonElevation.level4, color = IconButtonColor.Inverse, @@ -265,103 +225,6 @@ private fun HomeScreenTopBar(uiState: DashboardUiState, mainNavController: NavCo } } -@Composable -private fun DashboardProgramItem( - programUiState: DashboardProgramUiState, - onProgramClick: () -> Unit -) { - Column { - Text(text = programUiState.name, style = HorizonTypography.h2) - HorizonSpace(SpaceSize.SPACE_12) - Text(text = stringResource(R.string.dashboard_viewProgram), style = HorizonTypography.p1) - HorizonSpace(SpaceSize.SPACE_24) - Button(label = stringResource(R.string.dashboard_viewProgramButton), color = ButtonColor.Institution, onClick = onProgramClick) - HorizonSpace(SpaceSize.SPACE_24) - } -} - -@Composable -private fun DashboardCourseItem( - courseItem: DashboardCourseUiState, - onClick: () -> Unit, - onCourseClick: () -> Unit, - modifier: Modifier = Modifier, - onProgramClick: (String) -> Unit = {} -) { - Column(modifier) { - Column( - Modifier - .clip(HorizonCornerRadius.level1_5) - .clickable { - onCourseClick() - }) { - if (courseItem.parentPrograms.isNotEmpty()) { - ProgramsText(programs = courseItem.parentPrograms, onProgramClick = onProgramClick) - HorizonSpace(SpaceSize.SPACE_12) - } - Text(text = courseItem.courseName, style = HorizonTypography.h1) - HorizonSpace(SpaceSize.SPACE_12) - ProgressBar(progress = courseItem.courseProgress) - HorizonSpace(SpaceSize.SPACE_36) - } - if (courseItem.completed) { - Text(text = stringResource(R.string.dashboard_courseCompleted), style = HorizonTypography.h3) - HorizonSpace(SpaceSize.SPACE_12) - Text(text = stringResource(R.string.dashboard_courseCompletedDescription), style = HorizonTypography.p1) - } else { - Text(text = stringResource(R.string.dashboard_resumeLearning), style = HorizonTypography.h3) - HorizonSpace(SpaceSize.SPACE_12) - LearningObjectCard( - LearningObjectCardState( - moduleTitle = courseItem.nextModuleName.orEmpty(), - learningObjectTitle = courseItem.nextModuleItemName.orEmpty(), - progressLabel = courseItem.progressLabel, - remainingTime = courseItem.remainingTime, - dueDate = courseItem.dueDate, - learningObjectType = courseItem.learningObjectType, - onClick = onClick - ) - ) - } - HorizonSpace(SpaceSize.SPACE_24) - } -} - -@Composable -private fun ProgramsText( - programs: List, - onProgramClick: (String) -> Unit -) { - val programsAnnotated = buildAnnotatedString { - programs.forEachIndexed { i, program -> - if (i > 0) append(", ") - withLink( - LinkAnnotation.Clickable( - tag = program.programId, - styles = TextLinkStyles( - style = SpanStyle(textDecoration = TextDecoration.Underline) - ), - linkInteractionListener = { _ -> onProgramClick(program.programId) } - ) - ) { - append(program.programName) - } - } - } - - // String resource can't work with annotated string so we need a temporary placeholder - val template = stringResource(R.string.learnScreen_partOfProgram, "__PROGRAMS__") - - val fullText = buildAnnotatedString { - val parts = template.split("__PROGRAMS__") - append(parts[0]) - append(programsAnnotated) - if (parts.size > 1) append(parts[1]) - } - - Text(style = HorizonTypography.p1, text = fullText) -} - @Composable private fun NotificationPermissionRequest() { val context = LocalContext.current diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt index 6fc5a4fc90..6e183aff07 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt @@ -15,52 +15,11 @@ */ package com.instructure.horizon.features.dashboard -import com.instructure.horizon.horizonui.platform.LoadingState -import com.instructure.horizon.model.LearningObjectType -import java.util.Date - data class DashboardUiState( val logoUrl: String = "", - val programsUiState: List = emptyList(), - val coursesUiState: List = emptyList(), - val invitesUiState: List = emptyList(), - val loadingState: LoadingState = LoadingState(), val unreadCountState: DashboardUnreadState = DashboardUnreadState(), ) -data class DashboardCourseUiState( - val courseId: Long, - val courseName: String, - val courseProgress: Double, - val completed: Boolean = false, - val nextModuleName: String? = null, - val nextModuleItemName: String? = null, - val nextModuleItemId: Long? = null, - val progressLabel: String? = null, - val remainingTime: String? = null, - val learningObjectType: LearningObjectType? = null, - val dueDate: Date? = null, - val parentPrograms: List = emptyList() -) - -data class DashboardCourseProgram( - val programName: String = "", - val programId: String = "", -) - -data class CourseInviteUiState( - val courseId: Long, - val courseName: String, - val onAccept: () -> Unit, - val onDismiss: () -> Unit, - val acceptLoading: Boolean = false, -) - -data class DashboardProgramUiState( - val id: String, - val name: String, -) - data class DashboardUnreadState( val unreadConversations: Int = 0, val unreadNotifications: Int = 0, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt index a61202f4e8..1612f0bb60 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt @@ -15,27 +15,13 @@ */ package com.instructure.horizon.features.dashboard -import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.instructure.canvasapi2.managers.CourseWithProgress -import com.instructure.canvasapi2.managers.DashboardCourse -import com.instructure.canvasapi2.managers.graphql.Program -import com.instructure.canvasapi2.models.ModuleItem -import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch -import com.instructure.horizon.R -import com.instructure.horizon.horizonui.platform.LoadingState -import com.instructure.horizon.model.LearningObjectType -import com.instructure.journey.type.ProgramProgressCourseEnrollmentStatus import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.formatIsoDuration import com.instructure.pandautils.utils.poll import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -44,225 +30,34 @@ import javax.inject.Inject @HiltViewModel class DashboardViewModel @Inject constructor( private val dashboardRepository: DashboardRepository, - @ApplicationContext private val context: Context, private val themePrefs: ThemePrefs ) : ViewModel() { - private val _uiState = - MutableStateFlow(DashboardUiState(loadingState = LoadingState(onRefresh = ::refresh, onSnackbarDismiss = ::dismissSnackbar))) + private val _uiState = MutableStateFlow(DashboardUiState()) val uiState = _uiState.asStateFlow() init { viewModelScope.tryLaunch { - _uiState.update { it.copy(loadingState = it.loadingState.copy(isLoading = true)) } - loadData(forceNetwork = false) - _uiState.update { it.copy(loadingState = it.loadingState.copy(isLoading = false)) } + loadUnreadCount() + loadLogo() } catch { - _uiState.update { it.copy(loadingState = it.loadingState.copy(isLoading = false)) } - } - } - fun refresh() { - viewModelScope.tryLaunch { - _uiState.update { it.copy(loadingState = it.loadingState.copy(isRefreshing = true)) } - loadData(forceNetwork = true) - _uiState.update { it.copy(loadingState = it.loadingState.copy(isRefreshing = false)) } - } catch { - _uiState.update { it.copy(loadingState = it.loadingState.copy(isRefreshing = false)) } } } - private suspend fun loadData(forceNetwork: Boolean) { + private suspend fun loadLogo() { // We need to poll for the logo URL because the Dashboard already starts to load when the canvas theme is not yet applied at the first launch. poll( pollInterval = 50, maxAttempts = 10, block = { _uiState.update { it.copy(logoUrl = themePrefs.mobileLogoUrl) } }, - validate = { themePrefs.mobileLogoUrl.isNotEmpty() }) - val dashboardContent = dashboardRepository.getDashboardContent(forceNetwork = forceNetwork) - if (dashboardContent.isSuccess) { - val programs = try { - dashboardRepository.getPrograms(forceNetwork = forceNetwork) - } catch (e: Exception) { - emptyList() - } - val coursesResult = dashboardContent.dataOrThrow.courses - val courseUiStates = coursesResult.map { course -> - viewModelScope.async { - mapCourse(course, programs, forceNetwork) - } - }.awaitAll().filterNotNull() - val programsUiState = programs - .filter { program -> program.sortedRequirements.none { it.enrollmentStatus == ProgramProgressCourseEnrollmentStatus.ENROLLED } } - .map { DashboardProgramUiState(it.id, it.name) } - val inviteResults = dashboardContent.dataOrThrow.courseInvites - val invites = inviteResults.map { courseInvite -> - CourseInviteUiState(courseId = courseInvite.courseId, courseName = courseInvite.courseName, onAccept = { - updateAcceptLoadingForInvite(courseInvite.courseId, true) - viewModelScope.tryLaunch { - dashboardRepository.acceptInvite(courseInvite.courseId, courseInvite.enrollmentId) - refresh() - } catch { - _uiState.update { it.copy(loadingState = it.loadingState.copy(snackbarMessage = context.getString(R.string.dashboard_courseInviteFailed))) } - updateAcceptLoadingForInvite(courseInvite.courseId, false) - } - }, onDismiss = { - dismissInvite(courseInvite.courseId) - }) - } - loadUnreadCount() - _uiState.update { - it.copy( - programsUiState = programsUiState, - coursesUiState = courseUiStates, - invitesUiState = invites, - loadingState = it.loadingState.copy(isError = false) - ) - } - } else { - handleError() - } - } - - private suspend fun mapCourse( - dashboardCourse: DashboardCourse, - programs: List, - forceNetwork: Boolean - ): DashboardCourseUiState? { - val nextModuleId = dashboardCourse.nextUpModuleId - val nextModuleItemId = dashboardCourse.nextUpModuleItemId - val parentPrograms = - programs - .filter { program -> program.sortedRequirements.any { it.courseId == dashboardCourse.course.courseId } } - .map { DashboardCourseProgram(it.name, it.id) } - return if (nextModuleId != null && nextModuleItemId != null) { - createCourseUiState(dashboardCourse, parentPrograms) - } else if (dashboardCourse.course.progress < 100.0) { - - val modules = dashboardRepository.getFirstPageModulesWithItems( - dashboardCourse.course.courseId, - forceNetwork = forceNetwork - ) - - if (modules.isSuccess) { - val nextModuleItemResult = modules.dataOrThrow.flatMap { module -> module.items }.firstOrNull() - val nextModuleResult = modules.dataOrThrow.find { module -> module.id == nextModuleItemResult?.moduleId } - - if (nextModuleItemResult == null || nextModuleResult == null) { - return null - } - createCourseUiState(dashboardCourse.course, nextModuleResult, nextModuleItemResult, parentPrograms) - } else { - handleError() - null - } - } else if (dashboardCourse.course.progress == 100.0) { - DashboardCourseUiState( - courseId = dashboardCourse.course.courseId, - courseName = dashboardCourse.course.courseName, - courseProgress = dashboardCourse.course.progress, - completed = true, - progressLabel = getProgressLabel(dashboardCourse.course.progress), - parentPrograms = parentPrograms - ) - } else { - handleError() - null - } - } - - private fun createCourseUiState( - dashboardCourse: DashboardCourse, parentPrograms: List - ) = DashboardCourseUiState( - courseId = dashboardCourse.course.courseId, - courseName = dashboardCourse.course.courseName, - courseProgress = dashboardCourse.course.progress, - nextModuleName = dashboardCourse.nextUpModuleTitle ?: "", - nextModuleItemId = dashboardCourse.nextUpModuleItemId, - nextModuleItemName = dashboardCourse.nextUpModuleItemTitle ?: "", - progressLabel = getProgressLabel(dashboardCourse.course.progress), - remainingTime = dashboardCourse.nextModuleItemEstimatedDuration?.formatIsoDuration(context), - learningObjectType = if (dashboardCourse.isNewQuiz) LearningObjectType.ASSESSMENT else LearningObjectType.fromApiString( - dashboardCourse.nextModuleItemType.orEmpty() - ), - dueDate = dashboardCourse.nextModuleItemDueDate, - parentPrograms = parentPrograms - ) - - private fun createCourseUiState( - course: CourseWithProgress, - nextModule: ModuleObject?, - nextModuleItem: ModuleItem, - parentPrograms: List - ) = DashboardCourseUiState( - courseId = course.courseId, - courseName = course.courseName, - courseProgress = course.progress, - nextModuleName = nextModule?.name ?: "", - nextModuleItemId = nextModuleItem.id, - nextModuleItemName = nextModuleItem.title ?: "", - progressLabel = getProgressLabel(course.progress), - remainingTime = nextModuleItem.estimatedDuration?.formatIsoDuration(context), - learningObjectType = if (nextModuleItem.quizLti) LearningObjectType.ASSESSMENT else LearningObjectType.fromApiString(nextModuleItem.type.orEmpty()), - dueDate = nextModuleItem.moduleDetails?.dueDate, - parentPrograms = parentPrograms - ) - - private fun getProgressLabel(progress: Double): String { - return when (progress) { - 0.0 -> { - context.getString(R.string.learningObject_pillStatusNotStarted).uppercase() - } - - 100.0 -> { - context.getString(R.string.learningObject_pillStatusCompleted).uppercase() - } - - else -> { - context.getString(R.string.learningObject_pillStatusInProgress).uppercase() - } - } - } - - private fun handleError() { - _uiState.update { - if (it.coursesUiState.isEmpty()) { - it.copy(loadingState = it.loadingState.copy(isError = true)) - } else { - it.copy(loadingState = it.loadingState.copy(snackbarMessage = context.getString(R.string.errorOccurred))) - } - } - } - - private fun dismissSnackbar() { - _uiState.update { - it.copy(loadingState = it.loadingState.copy(snackbarMessage = null)) - } - } - - private fun updateAcceptLoadingForInvite(courseId: Long, isLoading: Boolean) { - _uiState.update { currentState -> - val updatedInvites = currentState.invitesUiState.map { invite -> - if (invite.courseId == courseId) { - invite.copy(acceptLoading = isLoading) - } else { - invite - } - } - currentState.copy(invitesUiState = updatedInvites) - } - } - - private fun dismissInvite(courseId: Long) { - _uiState.update { currentState -> - val updatedInvites = currentState.invitesUiState.filterNot { it.courseId == courseId } - currentState.copy(invitesUiState = updatedInvites) - } + validate = { themePrefs.mobileLogoUrl.isNotEmpty() } + ) } private suspend fun loadUnreadCount() { val unreadCounts = dashboardRepository.getUnreadCounts(true) - val unreadConversations = unreadCounts.firstOrNull { it.type == "Conversation" }?.count ?: 0 + val unreadConversations = unreadCounts.firstOrNull { it.type == "Conversation" }?.unreadCount ?: 0 val unreadNotifications = unreadCounts.filter { it.type == "Message" }.sumOf { it.unreadCount } _uiState.update { it.copy( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseRepository.kt new file mode 100644 index 0000000000..b85927add2 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseRepository.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard.course + +import com.instructure.canvasapi2.GetCoursesQuery +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.JourneyApiManager +import com.instructure.canvasapi2.managers.graphql.Program +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.utils.ApiPrefs +import javax.inject.Inject + +class DashboardCourseRepository @Inject constructor( + private val horizonGetCoursesManager: HorizonGetCoursesManager, + private val moduleApi: ModuleAPI.ModuleInterface, + private val apiPrefs: ApiPrefs, + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface, + private val journeyApiManager: JourneyApiManager, +) { + suspend fun getEnrollments(forceNetwork: Boolean): List { + return horizonGetCoursesManager.getEnrollments(apiPrefs.user?.id ?: -1, forceNetwork).dataOrThrow + } + + suspend fun acceptInvite(courseId: Long, enrollmentId: Long) { + return enrollmentApi.acceptInvite(courseId, enrollmentId, RestParams()).dataOrThrow + } + + suspend fun getPrograms(forceNetwork: Boolean = false): List { + return journeyApiManager.getPrograms(forceNetwork) + } + + suspend fun getFirstPageModulesWithItems(courseId: Long, forceNetwork: Boolean): List { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return moduleApi.getFirstPageModulesWithItems( + CanvasContext.Type.COURSE.apiString, + courseId, + params, + includes = listOf("estimated_durations") + ).dataOrThrow + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseSection.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseSection.kt new file mode 100644 index 0000000000..db5c3d23c2 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseSection.kt @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard.course + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.course.card.CardClickAction +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardContent +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardError +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardLoading +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardState +import com.instructure.horizon.features.home.HomeNavigationRoute +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.navigation.MainNavigationRoute +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlin.math.abs + +private const val cardAnimationRange: Float = 0.2f + +@Composable +fun DashboardCourseSection( + mainNavController: NavHostController, + homeNavController: NavHostController, + shouldRefresh: Boolean, + refreshState: MutableStateFlow> +) { + val viewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(shouldRefresh) { + if (shouldRefresh) { + refreshState.update { it + true } + state.onRefresh { + refreshState.update { it - true } + } + } + } + + DashboardCourseSection(state, mainNavController, homeNavController) +} + +@Composable +fun DashboardCourseSection( + state: DashboardCourseUiState, + mainNavController: NavHostController, + homeNavController: NavHostController +) { + when(state.state) { + DashboardItemState.LOADING -> { + DashboardCourseCardLoading(Modifier.padding(horizontal = 16.dp)) + } + DashboardItemState.ERROR -> { + DashboardCourseCardError({state.onRefresh {} }, Modifier.padding(horizontal = 16.dp)) + } + DashboardItemState.SUCCESS -> { + DashboardCourseSectionContent(state, mainNavController, homeNavController) + } + } +} + +@Composable +private fun DashboardCourseSectionContent( + state: DashboardCourseUiState, + mainNavController: NavHostController, + homeNavController: NavHostController +) { + val pagerstate = rememberPagerState { state.courses.size } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + state.programs.forEach { programCardState -> + DashboardCourseItem( + programCardState, + mainNavController, + homeNavController, + Modifier + .padding(horizontal = 16.dp) + ) + } + + var pagerHeight by remember { mutableIntStateOf(0) } + + HorizontalPager( + pagerstate, + contentPadding = PaddingValues(horizontal = 16.dp), + pageSpacing = 4.dp, + verticalAlignment = Alignment.Top, + modifier = Modifier + .animateContentSize() + .onGloballyPositioned { coordinates -> + pagerHeight = coordinates.size.height + } + ) { + var cardWidthList by remember { mutableStateOf(emptyMap()) } + val scaleAnimation by animateFloatAsState( + if (it == pagerstate.currentPage) { + (1 - abs(pagerstate.currentPageOffsetFraction.convertScaleRange())) + } else { + (1f - (cardAnimationRange * 2)) + (abs(pagerstate.currentPageOffsetFraction.convertScaleRange())) + }, + label = "DashboardCourseCardAnimation", + ) + var pagerItemHeight by remember { mutableIntStateOf(0) } + val verticalOffsetAnimation by animateIntAsState( + (pagerHeight - pagerItemHeight) / 2 + ) + val animationDirection = when { + it < pagerstate.currentPage -> 1 + it > pagerstate.currentPage -> -1 + else -> if (pagerstate.currentPageOffsetFraction > 0) 1 else -1 + } + DashboardCourseItem( + state.courses[it], + mainNavController, + homeNavController, + Modifier + .onGloballyPositioned { coordinates -> + pagerItemHeight = coordinates.size.height + cardWidthList = cardWidthList + (it to coordinates.size.width.toFloat()) + } + .offset { + IntOffset( + (animationDirection * ((cardWidthList[it] ?: 0f) / 2 * (1 - scaleAnimation))).toInt(), + verticalOffsetAnimation + ) + } + .scale(scaleAnimation) + ) + } + + DashboardCourseCardIndicator(pagerstate) + + Spacer(Modifier.height(16.dp)) + } +} + +private fun Float.convertScaleRange(): Float { + val oldMin = -0.5f + val oldMax = 0.5f + val newMin = -cardAnimationRange + val newMax = cardAnimationRange + return ((this - oldMin) / (oldMax - oldMin) ) * (newMax - newMin) + newMin +} + +@Composable +private fun DashboardCourseItem( + cardState: DashboardCourseCardState, + mainNavController: NavHostController, + homeNavController: NavHostController, + modifier: Modifier = Modifier +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier.fillMaxWidth() + ){ + DashboardCourseCardContent( + cardState, { handleClickAction(it, mainNavController, homeNavController) } + ) + } +} + +@Composable +private fun DashboardCourseCardIndicator(pagerState: PagerState) { + val selectedIndex = pagerState.currentPage + val offset = pagerState.currentPageOffsetFraction + + var scrollToIndex: Int? by remember { mutableStateOf(null) } + LaunchedEffect(scrollToIndex) { + if (scrollToIndex != null) { + pagerState.animateScrollToPage(scrollToIndex ?: return@LaunchedEffect) + scrollToIndex = null + } + } + + LazyRow( + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { + items(pagerState.pageCount) { itemIndex -> + val context = LocalContext.current + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(20.dp) + .padding(5.dp) + .border(1.dp, HorizonColors.Icon.medium(), CircleShape) + .clip(CircleShape) + .clickable { scrollToIndex = itemIndex } + .semantics { + selected = itemIndex == selectedIndex + contentDescription = context.getString( + R.string.a11y_dashboardPagerIndicatorContentDescription, + itemIndex + 1, + pagerState.pageCount + ) + } + ) { + if (itemIndex == selectedIndex) { + Box( + modifier = Modifier + .size(10.dp * (1 - abs(offset))) + .clip(CircleShape) + .background(HorizonColors.Icon.medium()) + ) + } else if (itemIndex == selectedIndex + (1 * if (offset > 0) 1 else -1)) { + Box( + modifier = Modifier + .size(10.dp * (abs(offset))) + .clip(CircleShape) + .background(HorizonColors.Icon.medium()) + ) + } + } + } + } +} + +private fun handleClickAction( + action: CardClickAction?, + mainNavController: NavHostController, + homeNavController: NavHostController +) { + when(action) { + is CardClickAction.Action -> { + action.onClick() + } + is CardClickAction.NavigateToCourse -> { + homeNavController.navigate(HomeNavigationRoute.Learn.withCourse(action.courseId)) { + popUpTo(homeNavController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = false + } + } + is CardClickAction.NavigateToModuleItem -> { + mainNavController.navigate( + MainNavigationRoute.ModuleItemSequence( + action.courseId, + action.moduleItemId + ) + ) + } + is CardClickAction.NavigateToProgram -> { + homeNavController.navigate(HomeNavigationRoute.Learn.withProgram(action.programId)) { + popUpTo(homeNavController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = false + } + } + else -> {} + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseUiState.kt new file mode 100644 index 0000000000..68d8fb3ca5 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseUiState.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard.course + +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardState + +data class DashboardCourseUiState( + val state: DashboardItemState = DashboardItemState.LOADING, + val programs: List = emptyList(), + val courses: List = emptyList(), + val onRefresh: (onFinished: () -> Unit)-> Unit = { } +) \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseViewModel.kt new file mode 100644 index 0000000000..4cdbd337c2 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardCourseViewModel.kt @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard.course + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.type.EnrollmentWorkflowState +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.course.card.CardClickAction +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardModuleItemState +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardState +import com.instructure.horizon.model.LearningObjectType +import com.instructure.journey.type.ProgramProgressCourseEnrollmentStatus +import com.instructure.pandautils.utils.formatIsoDuration +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class DashboardCourseViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val repository: DashboardCourseRepository +): ViewModel() { + private val _uiState = MutableStateFlow(DashboardCourseUiState(onRefresh = ::onRefresh)) + val uiState = _uiState.asStateFlow() + + init { + loadData() + } + + private fun loadData() { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + + viewModelScope.tryLaunch { + fetchData(forceNetwork = false) + _uiState.update { it.copy(state = DashboardItemState.SUCCESS) } + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + } + } + + private fun onRefresh(onFinished: () -> Unit = {}) { + viewModelScope.tryLaunch { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + fetchData(forceNetwork = true) + _uiState.update { it.copy(state = DashboardItemState.SUCCESS) } + onFinished() + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + onFinished() + } + } + + private suspend fun fetchData(forceNetwork: Boolean) { + var enrollments = repository.getEnrollments(forceNetwork) + val programs = repository.getPrograms(forceNetwork) + val invitations = enrollments.filter { it.state == EnrollmentWorkflowState.invited } + + // Accept invitations automatically + if (invitations.isNotEmpty()) { + invitations.forEach { enrollment -> + repository.acceptInvite( + enrollment.course?.id?.toLongOrNull() ?: return@forEach, + enrollment.id?.toLongOrNull() ?: return@forEach + ) + } + enrollments = repository.getEnrollments(true) + } + + + val courseCardStates = enrollments.mapToDashboardCourseCardState( + context, + programs = programs, + nextModuleForCourse = { courseId -> + fetchNextModuleState(courseId, forceNetwork) + }, + ).map { state -> + if (state.buttonState?.onClickAction is CardClickAction.Action) { + state.copy(buttonState = state.buttonState.copy( + onClickAction = CardClickAction.Action { + viewModelScope.tryLaunch { + updateCourseButtonState(state, isLoading = true) + state.buttonState.action() + onRefresh() + updateCourseButtonState(state, isLoading = false) + } catch { + updateCourseButtonState(state, isLoading = false) + } + }, + )) + } else state + } + + val programCardStates = programs + .filter { program -> program.sortedRequirements.none { it.enrollmentStatus == ProgramProgressCourseEnrollmentStatus.ENROLLED } } + .mapToDashboardCourseCardState(context) + + _uiState.update { + it.copy( + programs = programCardStates, + courses = courseCardStates + ) + } + } + + private suspend fun fetchNextModuleState(courseId: Long?, forceNetwork: Boolean): DashboardCourseCardModuleItemState? { + if (courseId == null) return null + val modules = repository.getFirstPageModulesWithItems(courseId, forceNetwork = forceNetwork) + val nextModuleItem = modules.flatMap { module -> module.items }.firstOrNull() + val nextModule = modules.find { module -> module.id == nextModuleItem?.moduleId } + if (nextModuleItem == null) { + return null + } + + return DashboardCourseCardModuleItemState( + moduleItemTitle = nextModuleItem.title.orEmpty(), + moduleItemType = if (nextModuleItem.quizLti) LearningObjectType.ASSESSMENT else LearningObjectType.fromApiString(nextModuleItem.type.orEmpty()), + dueDate = nextModuleItem.moduleDetails?.dueDate, + estimatedDuration = nextModuleItem.estimatedDuration?.formatIsoDuration(context), + onClickAction = CardClickAction.NavigateToModuleItem(courseId, nextModuleItem.id) + ) + } + + private fun updateCourseButtonState(state: DashboardCourseCardState, isLoading: Boolean) { + _uiState.update { + it.copy( + courses = it.courses.map { originalState -> + if (originalState.title == state.title && originalState.parentPrograms == state.parentPrograms) { + originalState.copy( + buttonState = originalState.buttonState?.copy( + isLoading = isLoading + ) + ) + } else { + originalState + } + } + ) + } + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardMapper.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardMapper.kt new file mode 100644 index 0000000000..2d1155587e --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/DashboardMapper.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard.course + +import android.content.Context +import com.instructure.canvasapi2.GetCoursesQuery +import com.instructure.canvasapi2.managers.graphql.Program +import com.instructure.canvasapi2.type.EnrollmentWorkflowState +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.course.card.CardClickAction +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardButtonState +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardModuleItemState +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardParentProgramState +import com.instructure.horizon.features.dashboard.course.card.DashboardCourseCardState + +internal suspend fun List.mapToDashboardCourseCardState( + context: Context, + programs: List, + nextModuleForCourse: suspend (Long?) -> DashboardCourseCardModuleItemState? +): List { + val completed = this.filter { it.isCompleted() }.map { it.mapCompleted(context, programs) } + val active = this.filter { it.isActive() }.map { it.mapActive(programs, nextModuleForCourse) } + return (active + completed).sortedByDescending { it.lastAccessed } +} + +internal fun List.mapToDashboardCourseCardState(context: Context,): List { + return this.map { program -> + DashboardCourseCardState( + title = program.name, + description = context.getString(R.string.dashboardNotStartedProgramDescription), + buttonState = DashboardCourseCardButtonState( + label = context.getString(R.string.dashboardNotStartedProgramDetailsLabel), + onClickAction = CardClickAction.NavigateToProgram(program.id), + ), + ) + } +} + +private fun GetCoursesQuery.Enrollment.isCompleted(): Boolean { + return this.state == EnrollmentWorkflowState.completed +} + +private fun GetCoursesQuery.Enrollment.isActive(): Boolean { + return this.state == EnrollmentWorkflowState.active +} + +private fun GetCoursesQuery.Enrollment.mapCompleted(context: Context, programs: List): DashboardCourseCardState { + return DashboardCourseCardState( + parentPrograms = programs + .filter { it.sortedRequirements.any { it.courseId == this.course?.id?.toLongOrNull() } } + .map { program -> + DashboardCourseCardParentProgramState( + programName = program.name, + programId = program.id, + onClickAction = CardClickAction.NavigateToProgram(program.id) + ) + }, + imageUrl = null, + title = this.course?.name.orEmpty(), + description = context.getString(R.string.dashboardCompletedCourseDetails), + progress = 1.0, + moduleItem = null, + buttonState = null, + onClickAction = CardClickAction.NavigateToCourse(this.course?.id?.toLongOrNull() ?: -1L) + ) +} + +private suspend fun GetCoursesQuery.Enrollment.mapActive( + programs: List, + nextModuleForCourse: suspend (Long?) -> DashboardCourseCardModuleItemState? +): DashboardCourseCardState { + return DashboardCourseCardState( + parentPrograms = programs + .filter { it.sortedRequirements.any { it.courseId == this.course?.id?.toLongOrNull() } } + .map { program -> + DashboardCourseCardParentProgramState( + programName = program.name, + programId = program.id, + onClickAction = CardClickAction.NavigateToProgram(program.id) + ) + }, + imageUrl = this.course?.image_download_url, + title = this.course?.name.orEmpty(), + description = null, + progress = this.course?.usersConnection?.nodes?.firstOrNull()?.courseProgression?.requirements?.completionPercentage ?: 0.0, + moduleItem = nextModuleForCourse(this.course?.id?.toLongOrNull()), + buttonState = null, + onClickAction = CardClickAction.NavigateToCourse(this.course?.id?.toLongOrNull() ?: -1L), + lastAccessed = this.lastActivityAt + ) +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCard.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCard.kt new file mode 100644 index 0000000000..b448d7e927 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCard.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard.course.card + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius +import com.instructure.horizon.horizonui.foundation.HorizonElevation +import com.instructure.horizon.horizonui.foundation.horizonShadow + +@Composable +fun DashboardCourseCard( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Box(modifier = modifier + .padding(horizontal = 8.dp) + .padding(bottom = 16.dp) + .horizonShadow(HorizonElevation.level4, shape = HorizonCornerRadius.level4, clip = true) + .background(color = HorizonColors.Surface.cardPrimary(), shape = HorizonCornerRadius.level4) + .widthIn(max = 400.dp) + ) { + content() + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardContent.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardContent.kt new file mode 100644 index 0000000000..bac0c634a1 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardContent.kt @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard.course.card + +import android.graphics.drawable.Drawable +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withLink +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.horizon.R +import com.instructure.horizon.horizonui.animation.shimmerEffect +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.molecules.ButtonColor +import com.instructure.horizon.horizonui.molecules.ButtonHeight +import com.instructure.horizon.horizonui.molecules.ButtonIconPosition +import com.instructure.horizon.horizonui.molecules.ButtonWidth +import com.instructure.horizon.horizonui.molecules.LoadingButton +import com.instructure.horizon.horizonui.molecules.Pill +import com.instructure.horizon.horizonui.molecules.PillCase +import com.instructure.horizon.horizonui.molecules.PillSize +import com.instructure.horizon.horizonui.molecules.PillStyle +import com.instructure.horizon.horizonui.molecules.PillType +import com.instructure.horizon.horizonui.molecules.ProgressBar +import com.instructure.horizon.horizonui.molecules.ProgressBarNumberStyle +import com.instructure.pandautils.utils.localisedFormatMonthDay +import java.util.Date + +@Composable +fun DashboardCourseCardContent( + state: DashboardCourseCardState, + handleOnClickAction: (CardClickAction?) -> Unit, + modifier: Modifier = Modifier +) { + DashboardCourseCard(modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = state.onClickAction != null) { + handleOnClickAction(state.onClickAction) + } + ) { + if (!state.imageUrl.isNullOrEmpty()) { + CourseImage(imageUrl = state.imageUrl) + } + Column( + modifier = Modifier + .padding(horizontal = 24.dp) + ) { + if (!state.parentPrograms.isNullOrEmpty()) { + Spacer(Modifier.height(16.dp)) + ProgramsText(state.parentPrograms, handleOnClickAction) + } + if (state.title.isNotEmpty()) { + Spacer(Modifier.height(16.dp)) + TitleText(state.title) + } + if (!state.description.isNullOrEmpty()) { + Spacer(Modifier.height(8.dp)) + DescriptionText(state.description) + } + if (state.progress != null) { + Spacer(Modifier.height(8.dp)) + CourseProgress(state.progress) + } + if (state.moduleItem != null) { + Spacer(Modifier.height(16.dp)) + ModuleItemCard(state.moduleItem, handleOnClickAction) + } + if (state.buttonState != null) { + Spacer(Modifier.height(16.dp)) + DashboardCardButton(state.buttonState, handleOnClickAction) + } + } + Spacer(Modifier.height(24.dp)) + } + } +} + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +private fun CourseImage(imageUrl: String) { + var isLoading by rememberSaveable { mutableStateOf(true) } + GlideImage( + imageUrl, + contentDescription = null, + contentScale = ContentScale.FillBounds, + requestBuilderTransform = { it.addListener( object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + isLoading = false + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + isLoading = false + return false + } + + }) }, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1.69f) + .shimmerEffect(isLoading) + ) +} + +@Composable +private fun ProgramsText( + programs: List, + handleOnClickAction: (CardClickAction?) -> Unit, +) { + val programsAnnotated = buildAnnotatedString { + programs.forEachIndexed { i, program -> + if (i > 0) append(", ") + withLink( + LinkAnnotation.Clickable( + tag = program.programId, + styles = TextLinkStyles( + style = SpanStyle(textDecoration = TextDecoration.Underline) + ), + linkInteractionListener = { _ -> handleOnClickAction(program.onClickAction) } + ) + ) { + append(program.programName) + } + } + } + + // String resource can't work with annotated string so we need a temporary placeholder + val template = stringResource(R.string.learnScreen_partOfProgram, "__PROGRAMS__") + + val fullText = buildAnnotatedString { + val parts = template.split("__PROGRAMS__") + append(parts[0]) + append(programsAnnotated) + if (parts.size > 1) append(parts[1]) + } + + Text(style = HorizonTypography.p1, text = fullText) +} + +@Composable +private fun TitleText(title: String) { + Text( + text = title, + style = HorizonTypography.h3, + color = HorizonColors.Text.title(), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) +} + +@Composable +private fun DescriptionText(description: String) { + Text( + text = description, + style = HorizonTypography.p1, + color = HorizonColors.Text.body(), + ) +} + +@Composable +private fun CourseProgress(progress: Double) { + ProgressBar(progress = progress, numberStyle = ProgressBarNumberStyle.OUTSIDE) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ModuleItemCard( + state: DashboardCourseCardModuleItemState, + handleOnClickAction: (CardClickAction?) -> Unit, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = HorizonColors.Surface + .institution() + .copy(alpha = 0.1f), shape = HorizonCornerRadius.level2 + ) + .clip(HorizonCornerRadius.level2) + .clickable { handleOnClickAction(state.onClickAction) } + .padding(16.dp) + ) { + Column { + Text( + text = state.moduleItemTitle, + style = HorizonTypography.p1, + color = HorizonColors.Text.body() + ) + Spacer(Modifier.height(12.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Pill( + label = stringResource(state.moduleItemType.stringRes), + size = PillSize.SMALL, + style = PillStyle.SOLID, + type = PillType.INVERSE, + case = PillCase.TITLE, + iconRes = state.moduleItemType.iconRes, + ) + + if (state.dueDate != null) { + Pill( + label = stringResource(R.string.learningobject_dueDate, state.dueDate.localisedFormatMonthDay()), + size = PillSize.SMALL, + style = PillStyle.SOLID, + type = PillType.INVERSE, + case = PillCase.TITLE, + iconRes = R.drawable.calendar_today, + ) + } + + if (state.estimatedDuration != null) { + Pill( + label = state.estimatedDuration, + size = PillSize.SMALL, + style = PillStyle.SOLID, + type = PillType.INVERSE, + case = PillCase.TITLE, + iconRes = R.drawable.calendar_today, + ) + } + } + } + } +} + +@Composable +private fun DashboardCardButton( + state: DashboardCourseCardButtonState, + handleOnClickAction: (CardClickAction?) -> Unit +) { + LoadingButton( + label = state.label, + height = ButtonHeight.SMALL, + width = ButtonWidth.RELATIVE, + color = ButtonColor.Black, + iconPosition = ButtonIconPosition.NoIcon, + onClick = { handleOnClickAction(state.onClickAction) }, + contentAlignment = Alignment.Center, + loading = state.isLoading, + ) +} + +@Composable +@Preview +private fun DashboardCourseCardWithModulePreview() { + ContextKeeper.appContext = LocalContext.current + + val state = DashboardCourseCardState( + parentPrograms = listOf( + DashboardCourseCardParentProgramState( + programName = "Program Name", + programId = "1", + onClickAction = CardClickAction.Action({}) + ) + ), + imageUrl = null, + title = "Course Title That Might Be Really Long and Go On Two Lines", + description = "This is a description of the course. It might be really long and go on multiple lines.", + progress = 45.0, + moduleItem = DashboardCourseCardModuleItemState( + moduleItemTitle = "Module Item Title That Might Be Really Long and Go On Two Lines", + moduleItemType = com.instructure.horizon.model.LearningObjectType.ASSIGNMENT, + dueDate = Date(), + estimatedDuration = "5 mins", + onClickAction = CardClickAction.Action({}) + ), + buttonState = DashboardCourseCardButtonState( + label = "Go to Course", + onClickAction = CardClickAction.Action({}) + ), + onClickAction = CardClickAction.Action({}) + ) + DashboardCourseCardContent(state, {}) +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardError.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardError.kt new file mode 100644 index 0000000000..cdfd494601 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardError.kt @@ -0,0 +1,54 @@ +package com.instructure.horizon.features.dashboard.course.card + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.instructure.horizon.R +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.molecules.Button +import com.instructure.horizon.horizonui.molecules.ButtonColor +import com.instructure.horizon.horizonui.molecules.ButtonHeight +import com.instructure.horizon.horizonui.molecules.ButtonIconPosition +import com.instructure.horizon.horizonui.molecules.ButtonWidth + +@Composable +fun DashboardCourseCardError( + onRetry: () -> Unit, + modifier: Modifier = Modifier +) { + DashboardCourseCard(modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 40.dp, horizontal = 24.dp) + ) { + Text( + text = stringResource(R.string.dashboardCourseCardErrorTitle), + style = HorizonTypography.h4, + color = HorizonColors.Text.title() + ) + Text( + text = stringResource(R.string.dashboardCourseCardErrorMessage), + style = HorizonTypography.p2, + color = HorizonColors.Text.timestamp() + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + label = stringResource(R.string.dashboardCourseCardRetryLabel), + height = ButtonHeight.SMALL, + width = ButtonWidth.RELATIVE, + color = ButtonColor.WhiteWithOutline, + iconPosition = ButtonIconPosition.End(R.drawable.restart_alt), + onClick = onRetry + ) + } + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardLoading.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardLoading.kt new file mode 100644 index 0000000000..6fea37ba5a --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardLoading.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard.course.card + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.instructure.horizon.horizonui.animation.shimmerEffect +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.SpaceSize + +@Composable +fun DashboardCourseCardLoading( + modifier: Modifier = Modifier, +) { + DashboardCourseCard(modifier.padding(bottom = 8.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding() + ) { + Box( + Modifier + .fillMaxWidth() + .aspectRatio(1.69f) + .shimmerEffect( + true, + shape = HorizonCornerRadius.level0, + ) + ) + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) { + HorizonSpace(SpaceSize.SPACE_8) + + Box( + Modifier + .fillMaxWidth() + .height(50.dp) + .shimmerEffect(true) + ) + + HorizonSpace(SpaceSize.SPACE_8) + + Box( + Modifier + .fillMaxWidth() + .height(25.dp) + .shimmerEffect( + true, + ) + ) + + HorizonSpace(SpaceSize.SPACE_8) + + Box( + Modifier + .fillMaxWidth() + .height(25.dp) + .shimmerEffect( + true, + shape = HorizonCornerRadius.level6, + backgroundColor = HorizonColors.Surface.institution().copy(alpha = 0.1f) + ) + ) + + HorizonSpace(SpaceSize.SPACE_8) + + Box( + Modifier + .fillMaxWidth() + .height(100.dp) + .shimmerEffect( + true, + shape = HorizonCornerRadius.level2, + backgroundColor = HorizonColors.Surface.institution().copy(alpha = 0.1f) + ) + ) + + HorizonSpace(SpaceSize.SPACE_24) + } + } + } +} + +@Composable +@Preview +private fun DashboardCourseCardLoadingPreview() { + DashboardCourseCardLoading() +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardState.kt new file mode 100644 index 0000000000..81503fae66 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardState.kt @@ -0,0 +1,44 @@ +package com.instructure.horizon.features.dashboard.course.card + +import com.instructure.horizon.model.LearningObjectType +import java.util.Date + +data class DashboardCourseCardState( + val parentPrograms: List? = null, + val imageUrl: String? = null, + val title: String, + val description: String? = null, + val progress: Double? = null, + val moduleItem: DashboardCourseCardModuleItemState? = null, + val buttonState: DashboardCourseCardButtonState? = null, + val onClickAction: CardClickAction? = null, + val lastAccessed: Date? = null, +) + +data class DashboardCourseCardParentProgramState( + val programName: String, + val programId: String, + val onClickAction: CardClickAction, +) + +data class DashboardCourseCardModuleItemState( + val moduleItemTitle: String, + val moduleItemType: LearningObjectType, + val dueDate: Date? = null, + val estimatedDuration: String? = null, + val onClickAction: CardClickAction, +) + +data class DashboardCourseCardButtonState( + val label: String, + val onClickAction: CardClickAction, + val isLoading: Boolean = false, + val action: suspend () -> Unit = { }, +) + +sealed class CardClickAction { + data class NavigateToProgram(val programId: String): CardClickAction() + data class NavigateToCourse(val courseId: Long): CardClickAction() + data class NavigateToModuleItem(val courseId: Long, val moduleItemId: Long): CardClickAction() + data class Action(val onClick: () -> Unit): CardClickAction() +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsScreen.kt index e789ac8a33..21d745391f 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsScreen.kt @@ -35,6 +35,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DASHBOARD_REFRESH import com.instructure.horizon.features.learn.program.components.CourseCardChipState import com.instructure.horizon.features.learn.program.components.CourseCardStatus import com.instructure.horizon.features.learn.program.components.ProgramCourseCardState @@ -44,7 +45,6 @@ import com.instructure.horizon.features.learn.program.components.ProgramProgress import com.instructure.horizon.features.learn.program.components.ProgramProgressState import com.instructure.horizon.features.learn.program.components.ProgramsProgressBar import com.instructure.horizon.features.learn.program.components.SequentialProgramProgressProperties -import com.instructure.horizon.features.moduleitemsequence.SHOULD_REFRESH_DASHBOARD import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonSpace import com.instructure.horizon.horizonui.foundation.HorizonTypography @@ -70,7 +70,7 @@ fun ProgramDetailsScreen(uiState: ProgramDetailsUiState, mainNavController: NavH remember(mainNavController.currentBackStackEntry) { mainNavController.getBackStackEntry(MainNavigationRoute.Home.route) } LaunchedEffect(uiState.shouldRefreshDashboard) { if (uiState.shouldRefreshDashboard) { - homeEntry.savedStateHandle[SHOULD_REFRESH_DASHBOARD] = true + homeEntry.savedStateHandle[DASHBOARD_REFRESH] = true uiState.onDashboardRefreshed() } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt index 15a633f413..30f63281d1 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt @@ -87,6 +87,7 @@ import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R import com.instructure.horizon.features.aiassistant.AiAssistantScreen import com.instructure.horizon.features.aiassistant.common.model.AiAssistContextSource +import com.instructure.horizon.features.dashboard.DASHBOARD_REFRESH import com.instructure.horizon.features.moduleitemsequence.content.LockedContentScreen import com.instructure.horizon.features.moduleitemsequence.content.assessment.AssessmentContentScreen import com.instructure.horizon.features.moduleitemsequence.content.assessment.AssessmentViewModel @@ -132,7 +133,6 @@ import com.instructure.pandautils.utils.orDefault import kotlinx.coroutines.launch import kotlin.math.abs -const val SHOULD_REFRESH_DASHBOARD = "shouldRefreshDashboard" const val SHOULD_REFRESH_LEARN_SCREEN = "shouldRefreshLearnScreen" @Composable @@ -294,7 +294,7 @@ private fun ModuleItemSequenceContent( remember(mainNavController.currentBackStackEntry) { mainNavController.getBackStackEntry(MainNavigationRoute.Home.route) } LaunchedEffect(uiState.shouldRefreshPreviousScreen) { if (uiState.shouldRefreshPreviousScreen) { - homeEntry.savedStateHandle[SHOULD_REFRESH_DASHBOARD] = true + homeEntry.savedStateHandle[DASHBOARD_REFRESH] = true homeEntry.savedStateHandle[SHOULD_REFRESH_LEARN_SCREEN] = true } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/animation/ShimmerAnimation.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/animation/ShimmerAnimation.kt new file mode 100644 index 0000000000..db057a69fd --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/animation/ShimmerAnimation.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2025 - 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.horizon.horizonui.animation + +import androidx.annotation.FloatRange +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.unit.IntSize +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius + + +@Composable +fun Modifier.shimmerEffect( + enabled: Boolean, + iterationDurationMillis: Int = 1000, + shape: Shape = HorizonCornerRadius.level1, + backgroundColor: Color = HorizonColors.PrimitivesGrey.grey14().copy(alpha = 0.5f), + shimmerColor: Color = HorizonColors.PrimitivesGrey.grey12().copy(alpha = 0.5f), + @FloatRange(from = 0.0, to = 1.0) shimmerRatio: Float = 0.5f, +): Modifier { + if (!enabled) return this + var size by remember { + mutableStateOf(IntSize.Zero) + } + val transition = rememberInfiniteTransition("ShimmerAnimationTransition") + + val startOffsetX by transition.animateFloat( + initialValue = -size.width.toFloat(), + targetValue = size.width.toFloat(), + animationSpec = infiniteRepeatable( + animation = tween(iterationDurationMillis) + ), + label = "ShimmerAnimation" + ) + + val backgroundFirstPart = (1 / shimmerRatio / 2).toInt() + val backgroundSecondPart = ((1 / shimmerRatio) - backgroundFirstPart).toInt() + val colors = buildList { + repeat(backgroundFirstPart) { add(backgroundColor) } + add(shimmerColor) + repeat(backgroundSecondPart) { add(backgroundColor) } + } + + return clip(shape).background( + brush = Brush.linearGradient( + colors = colors, + start = Offset(startOffsetX, 0f), + end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat()) + ) + ) + .onGloballyPositioned { + size = it.size + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Button.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Button.kt index f90f853df8..ba2d5cecd8 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Button.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Button.kt @@ -195,7 +195,7 @@ fun LoadingButton( label = label, height = height, width = width, - color = ButtonColor.Institution, + color = color, iconPosition = iconPosition, onClick = onClick, enabled = enabled, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Spinner.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Spinner.kt index c745d23b23..02e40c5173 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Spinner.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Spinner.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -46,7 +47,8 @@ fun Spinner( ) { val strokeBackground = if (hasStrokeBackground) HorizonColors.LineAndBorder.lineDivider() else Color.Transparent Box( - modifier = modifier, + modifier = modifier + .testTag("LoadingSpinner"), contentAlignment = Alignment.Center ) { if (progress != null) { diff --git a/libs/horizon/src/main/res/values/strings.xml b/libs/horizon/src/main/res/values/strings.xml index 91a3718391..d3fac667bb 100644 --- a/libs/horizon/src/main/res/values/strings.xml +++ b/libs/horizon/src/main/res/values/strings.xml @@ -357,4 +357,15 @@ Yesterday Unable to open Notification There are no external tools for this course. + Notebook + Notifications + Inbox + Course Card %1$d of %2$d + %1$d\%% complete + We weren’t able to load this content. + Please try again. + Retry + Welcome! View your program to enroll in your first course. + Program details + Congrats! You’ve completed your course. View your progress and scores on the Learn page. \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardRepositoryTest.kt new file mode 100644 index 0000000000..9d90d80dce --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardRepositoryTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard + +import com.instructure.canvasapi2.apis.UnreadCountAPI +import com.instructure.canvasapi2.models.UnreadNotificationCount +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class DashboardRepositoryTest { + private val unreadCountApi: UnreadCountAPI.UnreadCountsInterface = mockk(relaxed = true) + + private val notificationCounts = listOf( + UnreadNotificationCount( + type = "Message", + count = 5, + unreadCount = 10, + ), + UnreadNotificationCount( + type = "Conversation", + count = 2, + unreadCount = 5, + ), + UnreadNotificationCount( + type = "Announcement", + count = 1, + unreadCount = 3, + ), + ) + + @Before + fun setup() { + coEvery { unreadCountApi.getNotificationsCount(any()) } returns DataResult.Success(notificationCounts) + } + + @Test + fun `Test successful UnreadCount call`() = runTest { + val repository = getRepository() + val result = repository.getUnreadCounts(true) + assertEquals(3, result.size) + assertEquals(notificationCounts, result) + } + + private fun getRepository(): DashboardRepository { + return DashboardRepository(unreadCountApi) + } +} \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardViewModelTest.kt new file mode 100644 index 0000000000..4a6c879d8d --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardViewModelTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard + +import com.instructure.canvasapi2.models.UnreadNotificationCount +import com.instructure.pandautils.utils.ThemePrefs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class DashboardViewModelTest { + private val repository: DashboardRepository = mockk(relaxed = true) + private val themePrefs: ThemePrefs = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val notificationCounts = listOf( + UnreadNotificationCount( + type = "Message", + count = 5, + unreadCount = 10, + ), + UnreadNotificationCount( + type = "Conversation", + count = 2, + unreadCount = 5, + ), + UnreadNotificationCount( + type = "Announcement", + count = 1, + unreadCount = 3, + ), + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getUnreadCounts(any()) } returns notificationCounts + coEvery { themePrefs.mobileLogoUrl } returns "" + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel successfully loads and filters unread counts`() { + val viewModel = getViewModel() + coVerify { repository.getUnreadCounts(true) } + + val state = viewModel.uiState.value + assertEquals(5, state.unreadCountState.unreadConversations) + assertEquals(10, state.unreadCountState.unreadNotifications) + } + + @Test + fun `Test ViewModel loads logo URL from ThemePrefs`() { + val logoUrl = "https://example.com/logo.png" + coEvery { themePrefs.mobileLogoUrl } returns logoUrl + + val viewModel = getViewModel() + val state = viewModel.uiState.value + assertEquals(logoUrl, state.logoUrl) + } + + private fun getViewModel(): DashboardViewModel { + return DashboardViewModel(repository, themePrefs) + } +} \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseRepositoryTest.kt new file mode 100644 index 0000000000..d83886e96c --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseRepositoryTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard.course + +import com.instructure.canvasapi2.GetCoursesQuery +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.JourneyApiManager +import com.instructure.canvasapi2.managers.graphql.Program +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.type.EnrollmentWorkflowState +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.journey.type.ProgramVariantType +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class DashboardCourseRepositoryTest { + private val horizonGetCoursesManager: HorizonGetCoursesManager = mockk(relaxed = true) + private val moduleApi: ModuleAPI. ModuleInterface = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val enrollmentApi: EnrollmentAPI. EnrollmentInterface = mockk(relaxed = true) + private val journeyApiManager: JourneyApiManager = mockk(relaxed = true) + + private val userId = 1L + @Before + fun setup() { + every { apiPrefs.user?.id } returns userId + } + + @Test + fun `Test successful getEnrollments call`() = runTest { + val enrollments = listOf( + GetCoursesQuery.Enrollment( + "1", + EnrollmentWorkflowState.active, + null, + null + ) + ) + coEvery { horizonGetCoursesManager.getEnrollments(any(), any()) } returns DataResult.Success(enrollments) + val repository = getRepository() + + + val result = repository.getEnrollments(forceNetwork = true) + coVerify { horizonGetCoursesManager.getEnrollments(userId, true) } + assertEquals(enrollments, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed getEnrollments call`() = runTest { + coEvery { horizonGetCoursesManager.getEnrollments(any(), any()) } returns DataResult.Fail() + val repository = getRepository() + + repository.getEnrollments(forceNetwork = true) + coVerify { horizonGetCoursesManager.getEnrollments(userId, true) } + } + + @Test + fun `Test successful acceptInvite call`() = runTest { + val repository = getRepository() + coEvery { enrollmentApi.acceptInvite(any(), any(), any()) } returns DataResult.Success(Unit) + repository.acceptInvite(1, 1) + coVerify { enrollmentApi.acceptInvite(1, 1, any()) } + } + + @Test(expected = IllegalStateException::class) + fun `Test failed acceptInvite call`() = runTest { + val repository = getRepository() + coEvery { enrollmentApi.acceptInvite(any(), any(), any()) } returns DataResult.Fail() + repository.acceptInvite(1, 1) + coVerify { enrollmentApi.acceptInvite(1, 1, any()) } + } + + @Test + fun `Test successful getPrograms call`() = runTest { + val programs = listOf( + Program( + "1", + "Program 1", + null, + null, + null, + ProgramVariantType.LINEAR, + null, + emptyList() + ), + Program( + "2", + "Program 2", + null, + null, + null, + ProgramVariantType.NON_LINEAR, + null, + emptyList() + ), + ) + coEvery { journeyApiManager.getPrograms(any()) } returns programs + val repository = getRepository() + + val result = repository.getPrograms() + coVerify { journeyApiManager.getPrograms(any()) } + assertEquals(programs, result) + } + + @Test + fun `Test successful getFirstPageModulesWithItems call`() = runTest { + val courseId = 1L + val modules = listOf( + ModuleObject( + id = 1, + name = "Module 1", + items = listOf( + ModuleItem( + id = 1, + title = "Module Item 1", + moduleId = 1, + contentId = 1, + type = "Page", + estimatedDuration = "PT10M" + ) + ) + ), + ModuleObject( + id = 2, + name = "Module 2", + items = listOf( + ModuleItem( + id = 2, + title = "Module Item 2", + moduleId = 2, + contentId = 2, + type = "Assignment", + estimatedDuration = "PT10M" + ) + ) + ), + ) + coEvery { moduleApi.getFirstPageModulesWithItems(any(), any(), any(), any()) } returns DataResult.Success(modules) + val repository = getRepository() + + val result = repository.getFirstPageModulesWithItems(courseId, forceNetwork = true) + coVerify { moduleApi.getFirstPageModulesWithItems(CanvasContext.Type.COURSE.apiString, courseId, any(), listOf("estimated_durations")) } + assertEquals(modules, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed getFirstPageModulesWithItems call`() = runTest { + val courseId = 1L + coEvery { moduleApi.getFirstPageModulesWithItems(any(), any(), any(), any()) } returns DataResult.Fail() + val repository = getRepository() + + repository.getFirstPageModulesWithItems(courseId, forceNetwork = true) + coVerify { moduleApi.getFirstPageModulesWithItems(CanvasContext.Type.COURSE.apiString, courseId, any(), listOf("estimated_durations")) } + } + + private fun getRepository(): DashboardCourseRepository { + return DashboardCourseRepository(horizonGetCoursesManager, moduleApi, apiPrefs, enrollmentApi, journeyApiManager) + } +} \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseViewModelTest.kt new file mode 100644 index 0000000000..b551e22dc3 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseViewModelTest.kt @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.dashboard.course + +import android.content.Context +import com.instructure.canvasapi2.GetCoursesQuery +import com.instructure.canvasapi2.managers.graphql.Program +import com.instructure.canvasapi2.managers.graphql.ProgramRequirement +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.type.EnrollmentWorkflowState +import com.instructure.journey.type.ProgramProgressCourseEnrollmentStatus +import com.instructure.journey.type.ProgramVariantType +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class DashboardCourseViewModelTest { + private val context: Context = mockk(relaxed = true) + private var repository: DashboardCourseRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val courses = listOf( + GetCoursesQuery.Course( + id = "1", + name = "Course 1", + image_download_url = "url_1", + syllabus_body = "syllabus 1", + account = GetCoursesQuery.Account("Account 1"), + usersConnection = null + ), + GetCoursesQuery.Course( + id = "2", + name = "Course 2", + image_download_url = null, + syllabus_body = null, + account = null, + usersConnection = null + ), + GetCoursesQuery.Course( + id = "3", + name = "Course 3", + image_download_url = null, + syllabus_body = null, + account = null, + usersConnection = null + ), + GetCoursesQuery.Course( + id = "4", + name = "Course 4", + image_download_url = null, + syllabus_body = null, + account = null, + usersConnection = null + ), + ) + private val activeEnrollments = listOf( + GetCoursesQuery.Enrollment( + id = "1", + state = EnrollmentWorkflowState.active, + lastActivityAt = Date(), + course = courses[0] + ), + GetCoursesQuery.Enrollment( + id = "2", + state = EnrollmentWorkflowState.active, + lastActivityAt = Date(), + course = courses[1] + ), + ) + private val invitedEnrollments = listOf( + GetCoursesQuery.Enrollment( + id = "3", + state = EnrollmentWorkflowState.invited, + lastActivityAt = Date(), + course = courses[2] + ) + ) + private val completedEnrollments = listOf( + GetCoursesQuery.Enrollment( + id = "4", + state = EnrollmentWorkflowState.completed, + lastActivityAt = Date(), + course = courses[3] + ) + ) + private val programs = listOf( + Program( // Not started Program + id = "1", + name = "Program 1", + description = "Program 1 description", + startDate = null, + endDate = null, + variant = ProgramVariantType.LINEAR, + sortedRequirements = emptyList() + ), + Program( // Program with Course 2 + id = "2", + name = "Program 2", + description = "Program 2 description", + startDate = null, + endDate = null, + variant = ProgramVariantType.LINEAR, + sortedRequirements = listOf( + ProgramRequirement( + id = "1", + progressId = "1", + courseId = 2, + required = true, + progress = 5.0, + enrollmentStatus = ProgramProgressCourseEnrollmentStatus.ENROLLED + ) + ) + ) + ) + private val modules = listOf( + ModuleObject( + id = 1, + name = "Module 1", + items = listOf( + ModuleItem( + id = 1, + title = " Module Item 1", + moduleId = 1, + contentId = 1, + type = "Page", + estimatedDuration = "PT11M" + ) + ) + ) + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + + coEvery { repository.getEnrollments(any()) } returns activeEnrollments + invitedEnrollments + completedEnrollments + coEvery { repository.getPrograms(any()) } returns programs + coEvery { repository.acceptInvite(any(), any()) } just runs + coEvery { repository.getFirstPageModulesWithItems(any(), any()) } returns modules + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test course and empty programs are in the state list`() { + coEvery { repository.getEnrollments(any()) } returns activeEnrollments + completedEnrollments + val viewModel = getViewModel() + val state = viewModel.uiState.value + assertEquals(3, state.courses.size) + assertTrue(state.courses.any { it.title == "Course 1" }) + assertTrue(state.courses.any { it.title == "Course 2" }) + assertTrue(state.courses.any { it.title == "Course 4" }) + assertTrue(state.courses.none { it.title == "Course 3" }) + + assertEquals(1, state.programs.size) + assertTrue(state.programs.any { it.title == "Program 1" }) + assertTrue(state.programs.none { it.title == "Program 2" }) + } + + @Test + fun `Test course invitations are automatically accepted`() { + val viewModel = getViewModel() + coVerify { repository.acceptInvite(3, 3) } + } + + private fun getViewModel(): DashboardCourseViewModel { + return DashboardCourseViewModel(context, repository) + } +} \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationRepositoryTest.kt new file mode 100644 index 0000000000..14963c4529 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationRepositoryTest.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.notification + +import com.instructure.canvasapi2.apis.AccountNotificationAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.StreamAPI +import com.instructure.canvasapi2.managers.CourseWithProgress +import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.models.AccountNotification +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.StreamItem +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.LinkHeaders +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class NotificationRepositoryTest { + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val streamApi: StreamAPI.StreamInterface = mockk(relaxed = true) + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val accountNotificationApi: AccountNotificationAPI.AccountNotificationInterface = mockk(relaxed = true) + private val getCoursesManager: HorizonGetCoursesManager = mockk(relaxed = true) + + private val userId = 1L + @Before + fun setup() { + every { apiPrefs.user?.id } returns userId + } + + @Test + fun `Test successful Stream API notification items filtering`() = runTest { + val validCourse = CourseWithProgress( + courseId = 1L, + courseName = "Course 1", + courseSyllabus = "", + progress = 5.0 + ) + val notValidCourse = CourseWithProgress( + courseId = 2L, + courseName = "Course 2", + courseSyllabus = "", + progress = 0.0 + ) + val conversationStreamItem = StreamItem( + type = "Conversation", + course_id = validCourse.courseId, + ) + val messageStreamItem = StreamItem( + type = "Message", + course_id = validCourse.courseId, + ) + val discussionStreamItem = StreamItem( + type = "DiscussionTopic", + course_id = validCourse.courseId, + ) + val announcementStreamItem = StreamItem( + type = "Announcement", + course_id = validCourse.courseId, + ) + val notValidAnnouncementStreamItem = StreamItem( + type = "Announcement", + course_id = notValidCourse.courseId, + ) + val dueDateStreamItem = StreamItem( + type = "Message", + notificationCategory = "Due Date", + course_id = validCourse.courseId, + ) + val scoredGradeStreamItem = StreamItem( + type = "Message", + grade = "A", + course_id = validCourse.courseId, + ) + val scoredScoreStreamItem = StreamItem( + type = "Message", + score = 95.0, + course_id = validCourse.courseId, + ) + val gradingPeriodStreamItem = StreamItem( + type = "Message", + notificationCategory = "Grading Policies", + course_id = validCourse.courseId, + ) + val streamItems = listOf(conversationStreamItem, messageStreamItem, discussionStreamItem, announcementStreamItem, notValidAnnouncementStreamItem, dueDateStreamItem, scoredGradeStreamItem, scoredScoreStreamItem, gradingPeriodStreamItem) + + coEvery { streamApi.getUserStream(any()) } returns DataResult.Success(streamItems) + coEvery { getCoursesManager.getCoursesWithProgress(any(), any()) } returns DataResult.Success(listOf(validCourse)) + + val result = getRepository().getNotifications(forceRefresh = true) + + assertEquals(5, result.size) + assertTrue(result.contains(announcementStreamItem)) + assertTrue(result.contains(dueDateStreamItem)) + assertTrue(result.contains(scoredGradeStreamItem)) + assertTrue(result.contains(scoredScoreStreamItem)) + assertTrue(result.contains(gradingPeriodStreamItem)) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed Stream API call`() = runTest { + coEvery { getCoursesManager.getCoursesWithProgress(any(), any()) } returns DataResult.Success(emptyList()) + coEvery { streamApi.getUserStream(any()) } returns DataResult.Fail() + getRepository().getNotifications(true) + } + + @Test + fun `Test successful Global Announcements depagination`() = runTest { + val announcement1 = AccountNotification(id = 1L, subject = "Announcement 1") + val announcement2 = AccountNotification(id = 2L, subject = "Announcement 2") + + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns + DataResult.Success( + listOf(announcement1), + LinkHeaders(nextUrl = "nextPageUrl") + ) + coEvery { accountNotificationApi.getNextPageNotifications("nextPageUrl", any()) } returns + DataResult.Success( + listOf(announcement2), + ) + + val result = getRepository().getGlobalAnnouncements(true) + assertEquals(2, result.size) + assertEquals(listOf(announcement1, announcement2), result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed Global Announcements depagination`() = runTest { + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns DataResult.Fail() + getRepository().getGlobalAnnouncements(true) + } + + @Test + fun `Test successful getCourse by id`() = runTest { + val id = 1L + val course = Course(id = id, name = "Course 1") + coEvery { courseApi.getCourse(id, any()) } returns DataResult.Success(course) + val result = getRepository().getCourse(id) + assertEquals(course, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed getCourse by id`() = runTest { + coEvery { courseApi.getCourse(any(), any()) } returns DataResult.Fail() + getRepository().getCourse(1L) + } + + private fun getRepository(): NotificationRepository { + return NotificationRepository(apiPrefs, streamApi, courseApi, accountNotificationApi, getCoursesManager) + } +} \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationViewModelTest.kt new file mode 100644 index 0000000000..e68a13818b --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationViewModelTest.kt @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.notification + +import android.content.Context +import com.instructure.canvasapi2.models.AccountNotification +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.StreamItem +import com.instructure.canvasapi2.utils.toApiString +import com.instructure.canvasapi2.utils.toDate +import com.instructure.horizon.R +import com.instructure.horizon.features.inbox.HorizonInboxItemType +import com.instructure.horizon.features.inbox.navigation.HorizonInboxRoute +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class NotificationViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: NotificationRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val course = Course(1L, "Course 1") + private val globalAnnouncement = AccountNotification( + id = 1L, + subject = "Global Announcement 1", + startAt = Date().toApiString(), + ) + val streamItems = listOf( + StreamItem( + id = 1, + type = "Message", + title = "Scored item", + score = 5.0, + course_id = course.id, + htmlUrl = "deeplinkUrl1", + assignment = Assignment( + id = 1L, + name = "Assignment 1", + htmlUrl = "assignmentUrl1" + ) + ), + StreamItem( + id = 2, + type = "Message", + notificationCategory = "Grading Policies", + title = "Grading period item", + course_id = course.id, + htmlUrl = "deeplinkUrl2" + ), + StreamItem( + id = 3, + type = "Message", + notificationCategory = "Due date", + title = "Due Date", + course_id = course.id, + htmlUrl = "deeplinkUrl3" + ), + StreamItem( + id = 4, + type = "Announcement", + course_id = course.id, + htmlUrl = "deeplinkUrl4", + title = "Announcement 1" + ), + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + every { context.getString(R.string.notificationsAnnouncementCategoryLabel) } returns "Announcement" + every { context.getString(R.string.notificationsDueDateCategoryLabel) } returns "Due date" + every { context.getString(R.string.notificationsScoreCategoryLabel) } returns "Score" + every { context.getString(R.string.notificationsScoreChangedCategoryLabel) } returns "Score changed" + every { context.getString(R.string.notificationsScoredItemTitle, "Scored item") } returns "Scored item's score is now available" + coEvery { repository.getCourse(course.id) } returns course + coEvery { repository.getGlobalAnnouncements(any()) } returns listOf(globalAnnouncement) + coEvery { repository.getNotifications(any()) } returns streamItems + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel mapping to NotificationItems`() { + val viewModel = getViewModel() + val state = viewModel.uiState.value + assertEquals(5, state.notificationItems.size) + assertTrue(state.notificationItems.contains( + NotificationItem( + category = NotificationItemCategory( + "Score changed", + StatusChipColor.Violet + ), + title = "Scored item's score is now available", + courseLabel = null, + date = null, + isRead = false, + route = NotificationRoute.DeepLink("assignmentUrl1") + ) + )) + assertTrue(state.notificationItems.contains( + NotificationItem( + category = NotificationItemCategory( + "Score", + StatusChipColor.Violet + ), + title = "Grading period item", + courseLabel = null, + date = null, + isRead = false, + route = NotificationRoute.DeepLink("deeplinkUrl2") + ) + )) + assertTrue(state.notificationItems.contains( + NotificationItem( + category = NotificationItemCategory( + "Due date", + StatusChipColor.Honey + ), + title = "Due Date", + courseLabel = null, + date = null, + isRead = false, + route = NotificationRoute.DeepLink("deeplinkUrl3") + ) + )) + assertTrue(state.notificationItems.contains( + NotificationItem( + category = NotificationItemCategory( + "Announcement", + StatusChipColor.Sky + ), + title = "Announcement 1", + courseLabel = course.name, + date = null, + isRead = false, + route = NotificationRoute.DeepLink("deeplinkUrl4") + ) + )) + assertTrue(state.notificationItems.contains( + NotificationItem( + category = NotificationItemCategory( + "Announcement", + StatusChipColor.Sky + ), + title = "Global Announcement 1", + courseLabel = null, + date = globalAnnouncement.startAt.toDate(), + isRead = true, + route = NotificationRoute.ExplicitRoute( + HorizonInboxRoute.InboxDetails.route( + type = HorizonInboxItemType.AccountNotification, + id = 1L, + courseId = null + ) + ) + ) + )) + } + + private fun getViewModel(): NotificationViewModel { + return NotificationViewModel(context, repository) + } +} \ No newline at end of file From 38da0117b95310e286f6f18b9bc150234d824388 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:45:19 +0200 Subject: [PATCH 19/94] [MBL-19176][Student] K5: K5 Student is not able to open LTI tools from 'Resources' page refs: MBL-19176 affects: Student release note: Fixed an issue that occurred when opening LTI Tools from the K5 Resources screen. --- .../pandautils/features/lti/LtiLaunchRepository.kt | 3 ++- .../features/lti/LtiLaunchRepositoryTest.kt | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchRepository.kt index 19600623dd..735f174381 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchRepository.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchRepository.kt @@ -30,7 +30,8 @@ class LtiLaunchRepository( suspend fun getLtiFromAuthenticationUrl(url: String, ltiTool: LTITool?): LTITool { val params = RestParams(isForceReadFromNetwork = true) return ltiTool?.let { - assignmentApi.getExternalToolLaunchUrl(ltiTool.courseId, ltiTool.id, ltiTool.assignmentId, restParams = params).dataOrNull + val courseId = if (ltiTool.courseId == 0L) ltiTool.contextId ?: 0L else ltiTool.courseId + assignmentApi.getExternalToolLaunchUrl(courseId, ltiTool.id, ltiTool.assignmentId, restParams = params).dataOrNull } ?: launchDefinitionsApi.getLtiFromAuthenticationUrl(url, RestParams(isForceReadFromNetwork = true)).dataOrThrow } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/lti/LtiLaunchRepositoryTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/lti/LtiLaunchRepositoryTest.kt index 097cc7b8b8..b1dc00003a 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/lti/LtiLaunchRepositoryTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/lti/LtiLaunchRepositoryTest.kt @@ -132,4 +132,17 @@ class LtiLaunchRepositoryTest { assertEquals(url, result) } + + @Test + fun `Get lti from authentication url uses contextId when courseId is 0`() = runTest { + val url = "https://www.instructure.com" + val ltiTool = LTITool(courseId = 0, contextId = 123, id = 2, assignmentId = 3) + val expected = LTITool() + coEvery { assignmentApi.getExternalToolLaunchUrl(123, ltiTool.id, ltiTool.assignmentId, any(), any()) } returns DataResult.Success(expected) + + val result = repository.getLtiFromAuthenticationUrl(url, ltiTool) + + coVerify { assignmentApi.getExternalToolLaunchUrl(123, ltiTool.id, ltiTool.assignmentId, any(), any()) } + assertEquals(expected, result) + } } \ No newline at end of file From c931d5df96c70d6a16fabecba00a68263c80cc56 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:50:28 +0200 Subject: [PATCH 20/94] [MBL-19024][Teacher] Non-Teacher Account Navigation: App Redirect Fails to Open Correct Application (#3272) refs: MBL-19024 affects: Teacher release note: Fixed a bug where the Play Store/other app wouldn't open when clicking it's icon on the not a teacher screen. * Open app or play store when clicked * Vibe coded fixes * Fixed crash --- apps/teacher/src/main/AndroidManifest.xml | 1 + .../teacher/fragments/NotATeacherFragment.kt | 30 ++++++++++++------- .../teacher/utils/BaseAppManager.kt | 3 ++ .../res/layout/fragment_not_a_teacher.xml | 2 +- .../loginapi/login/tasks/LogoutTask.kt | 7 +++++ .../src/main/res/values/strings.xml | 1 + 6 files changed, 33 insertions(+), 11 deletions(-) diff --git a/apps/teacher/src/main/AndroidManifest.xml b/apps/teacher/src/main/AndroidManifest.xml index b77408b6ec..3552839f49 100644 --- a/apps/teacher/src/main/AndroidManifest.xml +++ b/apps/teacher/src/main/AndroidManifest.xml @@ -275,6 +275,7 @@ + diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/NotATeacherFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/NotATeacherFragment.kt index cdc3c5e645..80178a5f0b 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/NotATeacherFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/NotATeacherFragment.kt @@ -19,6 +19,8 @@ package com.instructure.teacher.fragments import android.content.Intent import android.net.Uri import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.View import com.instructure.loginapi.login.tasks.LogoutTask import com.instructure.pandautils.analytics.SCREEN_VIEW_NOT_A_TEACHER @@ -53,19 +55,11 @@ class NotATeacherFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) parentLink.setOnClickListener { - startActivity(playStoreIntent(PARENT_ID)) - TeacherLogoutTask( - LogoutTask.Type.LOGOUT_NO_LOGIN_FLOW, - alarmScheduler = alarmScheduler - ).execute() + openApp(PARENT_ID) } studentLink.setOnClickListener { - startActivity(playStoreIntent(CANVAS_ID)) - TeacherLogoutTask( - LogoutTask.Type.LOGOUT_NO_LOGIN_FLOW, - alarmScheduler = alarmScheduler - ).execute() + openApp(CANVAS_ID) } login.setOnClickListener { @@ -77,4 +71,20 @@ class NotATeacherFragment : BaseFragment() { } private fun playStoreIntent(s: String): Intent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(MARKET_URI_PREFIX + s) } + + private fun openApp(packageName: String) { + val launchIntent = requireContext().packageManager.getLaunchIntentForPackage(packageName) + if (launchIntent != null) { + startActivity(launchIntent) + } else { + startActivity(playStoreIntent(packageName)) + } + TeacherLogoutTask( + LogoutTask.Type.LOGOUT_NO_LOGIN_FLOW, + alarmScheduler = alarmScheduler + ).execute() + + // Finish the activity so it's removed from the stack + requireActivity().finish() + } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt b/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt index a14479de0b..15296bc223 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt @@ -99,6 +99,9 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager() { } override fun performLogoutOnAuthError() { + // Don't trigger another logout if we're already in a logout flow + if (LogoutTask.isLoggingOut) return + TeacherLogoutTask( LogoutTask.Type.LOGOUT, alarmScheduler = getScheduler() diff --git a/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml b/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml index 8b57a0da29..3c775708d8 100644 --- a/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml +++ b/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml @@ -37,7 +37,7 @@ android:layout_marginEnd="24dp" android:layout_marginStart="24dp" android:gravity="center" - android:text="@string/not_a_teacher_tap_to_visit_play_store" + android:text="@string/notATeacherDescription" android:textColor="@color/textDark" android:textSize="16sp"/> diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt index b089c61fd9..6d59908a6c 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt @@ -55,6 +55,12 @@ abstract class LogoutTask( QR_CODE_SWITCH } + companion object { + @Volatile + var isLoggingOut = false + private set + } + protected abstract fun onCleanup() protected abstract fun createLoginIntent(context: Context): Intent protected abstract fun createQRLoginIntent(context: Context, uri: Uri): Intent? @@ -67,6 +73,7 @@ abstract class LogoutTask( @Suppress("EXPERIMENTAL_FEATURE_WARNING") fun execute() { + isLoggingOut = true try { // Get the fcm token to delete the comm channel, then resume logout getFcmToken { registrationId -> diff --git a/libs/pandautils/src/main/res/values/strings.xml b/libs/pandautils/src/main/res/values/strings.xml index 5281f699ef..08cd2fdc95 100644 --- a/libs/pandautils/src/main/res/values/strings.xml +++ b/libs/pandautils/src/main/res/values/strings.xml @@ -499,4 +499,5 @@ %d Min %d Mins + One of our other apps may be a better fit. Tap an icon to log in there. From 6a4ea0449a28fd49715e67597682f8b71a0976c9 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:35:00 +0200 Subject: [PATCH 21/94] Add missing pendoAccessToken to open source data (#3283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add pendoAccessToken property (with dummy value) to open_source_data for student, teacher, and parent apps to fix open source builds. This property is required by the build.gradle files but was missing from the open source configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- open_source_data/parent/private.properties | 2 ++ open_source_data/student/private.properties | 5 ++++- open_source_data/teacher/private.properties | 5 ++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/open_source_data/parent/private.properties b/open_source_data/parent/private.properties index 74ee087707..96d101881c 100644 --- a/open_source_data/parent/private.properties +++ b/open_source_data/parent/private.properties @@ -10,3 +10,5 @@ roboTestUsername=1 heapProductionId=1 heapStagingId=1 +# Pendo +pendoAccessToken=1 diff --git a/open_source_data/student/private.properties b/open_source_data/student/private.properties index c1ebe70042..b10a99a833 100644 --- a/open_source_data/student/private.properties +++ b/open_source_data/student/private.properties @@ -14,4 +14,7 @@ pronounTestStudentPassword=2 # Student User for Push Notifications pushNotificationsTestStudent=3 -pushNotificationsTestStudentPassword=4 \ No newline at end of file +pushNotificationsTestStudentPassword=4 + +# Pendo +pendoAccessToken=1 \ No newline at end of file diff --git a/open_source_data/teacher/private.properties b/open_source_data/teacher/private.properties index 7f20ef0d22..8e83d2f393 100644 --- a/open_source_data/teacher/private.properties +++ b/open_source_data/teacher/private.properties @@ -21,4 +21,7 @@ pronounTestTeacherPassword=2 # Teacher User for Push Notifications pushNotificationsTestTeacher=3 -pushNotificationsTestTeacherPassword=4 \ No newline at end of file +pushNotificationsTestTeacherPassword=4 + +# Pendo +pendoAccessToken=1 \ No newline at end of file From c1f4ba93280dda25ad301e48a4e23a9ddc3491f4 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:09:29 +0200 Subject: [PATCH 22/94] [MBL-18997][All] Disable send button while inbox attachment uploads (#3277) Test plan: See ticket. Button is disabled while upload is in progress. refs: MBL-18997 affects: Student, Teacher, Parent release note: Fixed a bug where inbox messages were sent without attachment in some cases. --- .../espresso/mockcanvas/HttpResponder.kt | 11 +- .../inbox/compose/InboxComposeFragment.kt | 4 + .../inbox/compose/InboxComposeViewModel.kt | 115 ++++++- .../features/inbox/utils/AttachmentCard.kt | 13 +- .../inbox/utils/AttachmentCardItem.kt | 16 +- .../compose/InboxComposeViewModelTest.kt | 299 +++++++++++++++++- 6 files changed, 431 insertions(+), 27 deletions(-) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/HttpResponder.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/HttpResponder.kt index 2231e1f695..f0553eed3d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/HttpResponder.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/HttpResponder.kt @@ -41,10 +41,6 @@ class HttpResponder( getMethod = onHandle } - fun HttpResponder.HEAD(onHandle: () -> Response) { - headMethod = onHandle - } - fun HttpResponder.POST(onHandle: () -> Response) { postMethod = onHandle } @@ -57,6 +53,10 @@ class HttpResponder( deleteMethod = onHandle } + fun HttpResponder.HEAD(onHandle: () -> Response) { + headMethod = onHandle + } + fun handle(): Response { val method = request.method return when { @@ -75,7 +75,8 @@ class HttpResponder( postMethod?.let { println("POST $current\n") } putMethod?.let { println("PUT $current\n") } deleteMethod?.let { println("DELETE $current\n") } - return getMethod != null || postMethod != null || putMethod != null || deleteMethod != null + headMethod?.let { println("HEAD $current\n") } + return getMethod != null || postMethod != null || putMethod != null || deleteMethod != null || headMethod != null } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt index 9f8d3124c7..74f3227384 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt @@ -107,6 +107,10 @@ class InboxComposeFragment : BaseCanvasFragment(), FragmentInteractions, FileUpl return this } + override fun selectedUriStringsCallback(filePaths: List) { + viewModel.addUploadingAttachments(filePaths) + } + override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { workInfoLiveData.observe(viewLifecycleOwner) { workInfo -> viewModel.updateAttachments(uuid, workInfo) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt index 56fbe6364f..028ede9212 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt @@ -210,19 +210,110 @@ class InboxComposeViewModel @Inject constructor( } } - fun updateAttachments(uuid: UUID?, workInfo: WorkInfo) { - if (workInfo.state == WorkInfo.State.SUCCEEDED) { - viewModelScope.launch { - uuid?.let { uuid -> - val attachmentEntities = attachmentDao.findByParentId(uuid.toString()) - val status = workInfo.state.toAttachmentCardStatus() - attachmentEntities?.let { attachmentList -> - _uiState.update { it.copy(attachments = it.attachments + attachmentList.map { AttachmentCardItem(it.toApiModel(), status, false) }) } - attachmentDao.deleteAll(attachmentList) - } ?: sendScreenResult(context.getString(R.string.errorUploadingFile)) - } ?: sendScreenResult(context.getString(R.string.errorUploadingFile)) + fun addUploadingAttachments(filePaths: List) { + // Create placeholder attachments with UPLOADING status from file URIs + val placeholderAttachments = filePaths.mapIndexed { index, path -> + val fileName = path.substringAfterLast("/") + val attachment = com.instructure.canvasapi2.models.Attachment( + id = System.currentTimeMillis() + index, // Temporary ID until real one is assigned + filename = fileName, + displayName = fileName, + contentType = "" + ) + AttachmentCardItem(attachment, AttachmentStatus.UPLOADING, false) + } - } + _uiState.update { it.copy(attachments = it.attachments + placeholderAttachments) } + } + + fun updateAttachments(uuid: UUID?, workInfo: WorkInfo) { + viewModelScope.launch { + uuid?.let { workerId -> + val status = workInfo.state.toAttachmentCardStatus() + + when (workInfo.state) { + WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING -> { + // Update progress for uploading attachments + val progress = workInfo.progress + val uploadedSize = progress.getLong("PROGRESS_DATA_UPLOADED_SIZE", 0L) + val totalSize = progress.getLong("PROGRESS_DATA_TOTAL_SIZE", 0L) + val progressPercent = if (totalSize > 0) uploadedSize.toFloat() / totalSize.toFloat() else 0f + + // Find attachment with this workerId, or assign workerId to first placeholder without one + _uiState.update { currentState -> + var workerIdAssigned = false + currentState.copy( + attachments = currentState.attachments.map { attachment -> + when { + // Update attachment that already has this workerId + attachment.workerId == workerId.toString() -> { + attachment.copy(uploadProgress = progressPercent) + } + // Assign workerId to first placeholder without one + !workerIdAssigned && attachment.status == AttachmentStatus.UPLOADING && attachment.workerId == null -> { + workerIdAssigned = true + attachment.copy(workerId = workerId.toString(), uploadProgress = progressPercent) + } + else -> attachment + } + } + ) + } + } + WorkInfo.State.SUCCEEDED -> { + // Replace placeholder attachment with matching workerId with real uploaded attachments + val attachmentEntities = attachmentDao.findByParentId(workerId.toString()) + attachmentEntities?.let { attachmentList -> + // Create real uploaded attachments + val uploadedAttachments = attachmentList.map { + AttachmentCardItem(it.toApiModel(), status, false) + } + + // Remove placeholder with matching workerId, or first UPLOADING placeholder without workerId + _uiState.update { currentState -> + var placeholderRemoved = false + val filteredAttachments = currentState.attachments.filter { attachment -> + when { + attachment.workerId == workerId.toString() -> { + placeholderRemoved = true + false // Remove this placeholder + } + !placeholderRemoved && attachment.status == AttachmentStatus.UPLOADING && attachment.workerId == null -> { + placeholderRemoved = true + false // Remove first unassigned placeholder + } + else -> true + } + } + currentState.copy(attachments = filteredAttachments + uploadedAttachments) + } + attachmentDao.deleteAll(attachmentList) + } ?: sendScreenResult(context.getString(R.string.errorUploadingFile)) + } + WorkInfo.State.FAILED, WorkInfo.State.CANCELLED, WorkInfo.State.BLOCKED -> { + // Update placeholder with matching workerId to FAILED, or first UPLOADING placeholder without workerId + _uiState.update { currentState -> + var placeholderUpdated = false + currentState.copy( + attachments = currentState.attachments.map { attachment -> + when { + attachment.workerId == workerId.toString() -> { + placeholderUpdated = true + attachment.copy(status = AttachmentStatus.FAILED) + } + !placeholderUpdated && attachment.status == AttachmentStatus.UPLOADING && attachment.workerId == null -> { + placeholderUpdated = true + attachment.copy(status = AttachmentStatus.FAILED, workerId = workerId.toString()) + } + else -> attachment + } + } + ) + } + sendScreenResult(context.getString(R.string.errorUploadingFile)) + } + } + } ?: sendScreenResult(context.getString(R.string.errorUploadingFile)) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCard.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCard.kt index 9ad4391dac..0be744708c 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCard.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCard.kt @@ -130,7 +130,18 @@ fun AttachmentCard( if (!attachmentCardItem.readOnly){ when (status) { AttachmentStatus.UPLOADING -> { - Loading() + val progress = attachmentCardItem.uploadProgress + if (progress != null && progress > 0f) { + // Show progress percentage + Text( + text = "${(progress * 100).toInt()}%", + color = colorResource(id = R.color.textDark), + fontSize = 14.sp + ) + } else { + // Show indeterminate loading spinner + Loading() + } } AttachmentStatus.UPLOADED -> { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCardItem.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCardItem.kt index a52a409c13..ce2e4ffd6f 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCardItem.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCardItem.kt @@ -21,7 +21,9 @@ import com.instructure.canvasapi2.models.Attachment data class AttachmentCardItem ( val attachment: Attachment, val status: AttachmentStatus, // TODO: Currently this is not used for proper state handling, but if the upload process will be refactored it can be useful - val readOnly: Boolean + val readOnly: Boolean, + val uploadProgress: Float? = null, // Upload progress from 0.0 to 1.0, null if not uploading + val workerId: String? = null // WorkManager UUID to track which upload this belongs to ) enum class AttachmentStatus { @@ -34,12 +36,12 @@ enum class AttachmentStatus { companion object { fun fromWorkInfoState(state: WorkInfo.State): AttachmentStatus { return when (state) { - WorkInfo.State.SUCCEEDED -> AttachmentStatus.UPLOADED - WorkInfo.State.FAILED -> AttachmentStatus.FAILED - WorkInfo.State.ENQUEUED -> AttachmentStatus.UPLOADING - WorkInfo.State.RUNNING -> AttachmentStatus.UPLOADING - WorkInfo.State.BLOCKED -> AttachmentStatus.FAILED - WorkInfo.State.CANCELLED -> AttachmentStatus.FAILED + WorkInfo.State.SUCCEEDED -> UPLOADED + WorkInfo.State.FAILED -> FAILED + WorkInfo.State.ENQUEUED -> UPLOADING + WorkInfo.State.RUNNING -> UPLOADING + WorkInfo.State.BLOCKED -> FAILED + WorkInfo.State.CANCELLED -> FAILED } } } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModelTest.kt index bd771f7b09..4ff278903d 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModelTest.kt @@ -496,9 +496,9 @@ class InboxComposeViewModelTest { @Test fun `Attachment removed`() { val viewmodel = getViewModel() - val attachment = Attachment() + val attachment = Attachment(id = 1) val attachmentEntity = com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity(attachment) - val attachmentCardItem = AttachmentCardItem(Attachment(), AttachmentStatus.UPLOADED, false) + val attachmentCardItem = AttachmentCardItem(attachment, AttachmentStatus.UPLOADED, false) val uuid = UUID.randomUUID() coEvery { attachmentDao.findByParentId(uuid.toString()) } returns listOf(attachmentEntity) viewmodel.updateAttachments(uuid, WorkInfo(UUID.randomUUID(), WorkInfo.State.SUCCEEDED, setOf(""))) @@ -510,6 +510,301 @@ class InboxComposeViewModelTest { assertEquals(0, viewmodel.uiState.value.attachments.size) } + @Test + fun `Attachment added with UPLOADING status on ENQUEUED state`() { + val viewmodel = getViewModel() + val filePaths = listOf("/storage/test.pdf") + + viewmodel.addUploadingAttachments(filePaths) + + assertEquals(1, viewmodel.uiState.value.attachments.size) + assertEquals(AttachmentStatus.UPLOADING, viewmodel.uiState.value.attachments.first().status) + assertEquals("test.pdf", viewmodel.uiState.value.attachments.first().attachment.displayName) + } + + @Test + fun `Attachment added with UPLOADING status on RUNNING state`() { + val viewmodel = getViewModel() + val filePaths = listOf("/storage/test.pdf") + + viewmodel.addUploadingAttachments(filePaths) + + assertEquals(1, viewmodel.uiState.value.attachments.size) + assertEquals(AttachmentStatus.UPLOADING, viewmodel.uiState.value.attachments.first().status) + assertEquals("test.pdf", viewmodel.uiState.value.attachments.first().attachment.displayName) + } + + @Test + fun `Send button disabled when attachment is uploading`() { + val viewmodel = getViewModel() + + // Set up required fields for sending + viewmodel.handleAction(ContextPickerActionHandler.ContextClicked(Course(id = 1, name = "Test Course"))) + viewmodel.handleAction(InboxComposeActionHandler.AddRecipient(Recipient(stringId = "1", name = "Test User"))) + viewmodel.handleAction(InboxComposeActionHandler.SubjectChanged(TextFieldValue("Test Subject"))) + viewmodel.handleAction(InboxComposeActionHandler.BodyChanged(TextFieldValue("Test Body"))) + + // At this point, send button should be enabled + assertEquals(true, viewmodel.uiState.value.isSendButtonEnabled) + + // Add uploading placeholder attachment + viewmodel.addUploadingAttachments(listOf("/storage/test.pdf")) + + // Send button should now be disabled + assertEquals(false, viewmodel.uiState.value.isSendButtonEnabled) + } + + @Test + fun `Send button enabled when all attachments uploaded`() { + val viewmodel = getViewModel() + val attachment = Attachment(id = 1, displayName = "test.pdf") + val attachmentEntity = com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity(attachment) + val uuid = UUID.randomUUID() + + // Set up required fields for sending + viewmodel.handleAction(ContextPickerActionHandler.ContextClicked(Course(id = 1, name = "Test Course"))) + viewmodel.handleAction(InboxComposeActionHandler.AddRecipient(Recipient(stringId = "1", name = "Test User"))) + viewmodel.handleAction(InboxComposeActionHandler.SubjectChanged(TextFieldValue("Test Subject"))) + viewmodel.handleAction(InboxComposeActionHandler.BodyChanged(TextFieldValue("Test Body"))) + + coEvery { attachmentDao.findByParentId(uuid.toString()) } returns listOf(attachmentEntity) + + // Start upload with placeholder + viewmodel.addUploadingAttachments(listOf("/storage/test.pdf")) + assertEquals(false, viewmodel.uiState.value.isSendButtonEnabled) + + // Complete upload - replaces placeholder with real attachment + viewmodel.updateAttachments(uuid, WorkInfo(UUID.randomUUID(), WorkInfo.State.SUCCEEDED, setOf(""))) + assertEquals(true, viewmodel.uiState.value.isSendButtonEnabled) + } + + @Test + fun `Attachment status updates from UPLOADING to UPLOADED on success`() { + val viewmodel = getViewModel() + val attachment = Attachment(id = 1, displayName = "test.pdf") + val attachmentEntity = com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity(attachment) + val uuid = UUID.randomUUID() + + coEvery { attachmentDao.findByParentId(uuid.toString()) } returns listOf(attachmentEntity) + + // Start upload with placeholder + viewmodel.addUploadingAttachments(listOf("/storage/test.pdf")) + assertEquals(AttachmentStatus.UPLOADING, viewmodel.uiState.value.attachments.first().status) + + // Complete upload - replaces placeholder + viewmodel.updateAttachments(uuid, WorkInfo(UUID.randomUUID(), WorkInfo.State.SUCCEEDED, setOf(""))) + assertEquals(AttachmentStatus.UPLOADED, viewmodel.uiState.value.attachments.first().status) + } + + @Test + fun `Attachment status updates from UPLOADING to FAILED on failure`() { + val viewmodel = getViewModel() + + // Start upload with placeholder + viewmodel.addUploadingAttachments(listOf("/storage/test.pdf")) + assertEquals(AttachmentStatus.UPLOADING, viewmodel.uiState.value.attachments.first().status) + + // Upload fails - updates placeholder status + viewmodel.updateAttachments(UUID.randomUUID(), WorkInfo(UUID.randomUUID(), WorkInfo.State.FAILED, setOf(""))) + assertEquals(AttachmentStatus.FAILED, viewmodel.uiState.value.attachments.first().status) + } + + @Test + fun `Multiple attachments with mixed upload states`() { + val viewmodel = getViewModel() + val attachment1 = Attachment(id = 1, displayName = "test1.pdf") + val attachment2 = Attachment(id = 2, displayName = "test2.pdf") + val attachmentEntity1 = com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity(attachment1) + val attachmentEntity2 = com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity(attachment2) + val uuid1 = UUID.randomUUID() + val uuid2 = UUID.randomUUID() + + // Set up required fields for sending + viewmodel.handleAction(ContextPickerActionHandler.ContextClicked(Course(id = 1, name = "Test Course"))) + viewmodel.handleAction(InboxComposeActionHandler.AddRecipient(Recipient(stringId = "1", name = "Test User"))) + viewmodel.handleAction(InboxComposeActionHandler.SubjectChanged(TextFieldValue("Test Subject"))) + viewmodel.handleAction(InboxComposeActionHandler.BodyChanged(TextFieldValue("Test Body"))) + + coEvery { attachmentDao.findByParentId(uuid1.toString()) } returns listOf(attachmentEntity1) + coEvery { attachmentDao.findByParentId(uuid2.toString()) } returns listOf(attachmentEntity2) + + // Start both uploads with placeholders + viewmodel.addUploadingAttachments(listOf("/storage/test1.pdf")) + viewmodel.addUploadingAttachments(listOf("/storage/test2.pdf")) + + assertEquals(2, viewmodel.uiState.value.attachments.size) + assertEquals(false, viewmodel.uiState.value.isSendButtonEnabled) + + // First upload completes - replaces first placeholder + viewmodel.updateAttachments(uuid1, WorkInfo(UUID.randomUUID(), WorkInfo.State.SUCCEEDED, setOf(""))) + + // Send button still disabled (second placeholder still uploading) + assertEquals(false, viewmodel.uiState.value.isSendButtonEnabled) + assertEquals(1, viewmodel.uiState.value.attachments.count { it.status == AttachmentStatus.UPLOADED }) + assertEquals(1, viewmodel.uiState.value.attachments.count { it.status == AttachmentStatus.UPLOADING }) + + // Second upload completes + viewmodel.updateAttachments(uuid2, WorkInfo(UUID.randomUUID(), WorkInfo.State.SUCCEEDED, setOf(""))) + + // Now send button should be enabled + assertEquals(true, viewmodel.uiState.value.isSendButtonEnabled) + assertEquals(2, viewmodel.uiState.value.attachments.count { it.status == AttachmentStatus.UPLOADED }) + } + + @Test + fun `No duplicate attachments added on multiple state updates`() { + val viewmodel = getViewModel() + + // Add placeholder once + viewmodel.addUploadingAttachments(listOf("/storage/test.pdf")) + assertEquals(1, viewmodel.uiState.value.attachments.size) + + // Multiple state updates should not add duplicates (placeholders already added) + viewmodel.updateAttachments(UUID.randomUUID(), WorkInfo(UUID.randomUUID(), WorkInfo.State.RUNNING, setOf(""))) + viewmodel.updateAttachments(UUID.randomUUID(), WorkInfo(UUID.randomUUID(), WorkInfo.State.RUNNING, setOf(""))) + + // Should still only have one attachment + assertEquals(1, viewmodel.uiState.value.attachments.size) + } + + @Test + fun `Concurrent uploads - one fails, other continues successfully`() { + val viewmodel = getViewModel() + val attachment1 = Attachment(id = 1, displayName = "test1.pdf") + val attachment2 = Attachment(id = 2, displayName = "test2.pdf") + val attachmentEntity1 = com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity(attachment1) + val attachmentEntity2 = com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity(attachment2) + val uuid1 = UUID.randomUUID() + val uuid2 = UUID.randomUUID() + + coEvery { attachmentDao.findByParentId(uuid1.toString()) } returns listOf(attachmentEntity1) + coEvery { attachmentDao.findByParentId(uuid2.toString()) } returns listOf(attachmentEntity2) + + // Start both uploads with placeholders + viewmodel.addUploadingAttachments(listOf("/storage/test1.pdf")) + viewmodel.addUploadingAttachments(listOf("/storage/test2.pdf")) + + // Assign workerIds to placeholders + viewmodel.updateAttachments(uuid1, WorkInfo(UUID.randomUUID(), WorkInfo.State.RUNNING, setOf(""))) + viewmodel.updateAttachments(uuid2, WorkInfo(UUID.randomUUID(), WorkInfo.State.RUNNING, setOf(""))) + + assertEquals(2, viewmodel.uiState.value.attachments.size) + assertEquals(AttachmentStatus.UPLOADING, viewmodel.uiState.value.attachments[0].status) + assertEquals(AttachmentStatus.UPLOADING, viewmodel.uiState.value.attachments[1].status) + + // First upload FAILS + viewmodel.updateAttachments(uuid1, WorkInfo(UUID.randomUUID(), WorkInfo.State.FAILED, setOf(""))) + + // Verify: First attachment is FAILED, second is still UPLOADING + assertEquals(2, viewmodel.uiState.value.attachments.size) + assertEquals(AttachmentStatus.FAILED, viewmodel.uiState.value.attachments.first { it.workerId == uuid1.toString() }.status) + assertEquals(AttachmentStatus.UPLOADING, viewmodel.uiState.value.attachments.first { it.workerId == uuid2.toString() }.status) + + // Second upload SUCCEEDS + viewmodel.updateAttachments(uuid2, WorkInfo(UUID.randomUUID(), WorkInfo.State.SUCCEEDED, setOf(""))) + + // Verify: First attachment still FAILED, second is UPLOADED + assertEquals(2, viewmodel.uiState.value.attachments.size) + assertEquals(AttachmentStatus.FAILED, viewmodel.uiState.value.attachments.first { it.workerId == uuid1.toString() }.status) + assertEquals(AttachmentStatus.UPLOADED, viewmodel.uiState.value.attachments.first { it.attachment.id == 2L }.status) + } + + @Test + fun `Concurrent uploads - both fail independently`() { + val viewmodel = getViewModel() + val uuid1 = UUID.randomUUID() + val uuid2 = UUID.randomUUID() + + // Start both uploads with placeholders + viewmodel.addUploadingAttachments(listOf("/storage/test1.pdf")) + viewmodel.addUploadingAttachments(listOf("/storage/test2.pdf")) + + // Assign workerIds to placeholders + viewmodel.updateAttachments(uuid1, WorkInfo(UUID.randomUUID(), WorkInfo.State.RUNNING, setOf(""))) + viewmodel.updateAttachments(uuid2, WorkInfo(UUID.randomUUID(), WorkInfo.State.RUNNING, setOf(""))) + + assertEquals(2, viewmodel.uiState.value.attachments.size) + + // First upload fails + viewmodel.updateAttachments(uuid1, WorkInfo(UUID.randomUUID(), WorkInfo.State.FAILED, setOf(""))) + + // Verify: Only first attachment is FAILED, second is still UPLOADING + assertEquals(AttachmentStatus.FAILED, viewmodel.uiState.value.attachments.first { it.workerId == uuid1.toString() }.status) + assertEquals(AttachmentStatus.UPLOADING, viewmodel.uiState.value.attachments.first { it.workerId == uuid2.toString() }.status) + + // Second upload also fails + viewmodel.updateAttachments(uuid2, WorkInfo(UUID.randomUUID(), WorkInfo.State.FAILED, setOf(""))) + + // Verify: Both attachments are FAILED + assertEquals(2, viewmodel.uiState.value.attachments.size) + assertEquals(AttachmentStatus.FAILED, viewmodel.uiState.value.attachments.first { it.workerId == uuid1.toString() }.status) + assertEquals(AttachmentStatus.FAILED, viewmodel.uiState.value.attachments.first { it.workerId == uuid2.toString() }.status) + } + + @Test + fun `Concurrent uploads - both succeed independently`() { + val viewmodel = getViewModel() + val attachment1 = Attachment(id = 1, displayName = "test1.pdf") + val attachment2 = Attachment(id = 2, displayName = "test2.pdf") + val attachmentEntity1 = com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity(attachment1) + val attachmentEntity2 = com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity(attachment2) + val uuid1 = UUID.randomUUID() + val uuid2 = UUID.randomUUID() + + coEvery { attachmentDao.findByParentId(uuid1.toString()) } returns listOf(attachmentEntity1) + coEvery { attachmentDao.findByParentId(uuid2.toString()) } returns listOf(attachmentEntity2) + + // Start both uploads with placeholders + viewmodel.addUploadingAttachments(listOf("/storage/test1.pdf")) + viewmodel.addUploadingAttachments(listOf("/storage/test2.pdf")) + + // Assign workerIds + viewmodel.updateAttachments(uuid1, WorkInfo(UUID.randomUUID(), WorkInfo.State.RUNNING, setOf(""))) + viewmodel.updateAttachments(uuid2, WorkInfo(UUID.randomUUID(), WorkInfo.State.RUNNING, setOf(""))) + + assertEquals(2, viewmodel.uiState.value.attachments.size) + + // First upload succeeds + viewmodel.updateAttachments(uuid1, WorkInfo(UUID.randomUUID(), WorkInfo.State.SUCCEEDED, setOf(""))) + + // Verify: First replaced with real attachment, second still uploading + assertEquals(2, viewmodel.uiState.value.attachments.size) + assertEquals(1, viewmodel.uiState.value.attachments.count { it.status == AttachmentStatus.UPLOADED }) + assertEquals(1, viewmodel.uiState.value.attachments.count { it.status == AttachmentStatus.UPLOADING }) + + // Second upload succeeds + viewmodel.updateAttachments(uuid2, WorkInfo(UUID.randomUUID(), WorkInfo.State.SUCCEEDED, setOf(""))) + + // Verify: Both replaced with real attachments + assertEquals(2, viewmodel.uiState.value.attachments.size) + assertEquals(2, viewmodel.uiState.value.attachments.count { it.status == AttachmentStatus.UPLOADED }) + assertEquals(1L, viewmodel.uiState.value.attachments.first { it.attachment.id == 1L }.attachment.id) + assertEquals(2L, viewmodel.uiState.value.attachments.first { it.attachment.id == 2L }.attachment.id) + } + + @Test + fun `WorkerId assigned to placeholders tracks uploads independently`() { + val viewmodel = getViewModel() + val uuid1 = UUID.randomUUID() + val uuid2 = UUID.randomUUID() + + // Start both uploads + viewmodel.addUploadingAttachments(listOf("/storage/test1.pdf", "/storage/test2.pdf")) + + // Assign workerIds + viewmodel.updateAttachments(uuid1, WorkInfo(UUID.randomUUID(), WorkInfo.State.RUNNING, setOf(""))) + viewmodel.updateAttachments(uuid2, WorkInfo(UUID.randomUUID(), WorkInfo.State.RUNNING, setOf(""))) + + // Both should have workerIds assigned + assertEquals(2, viewmodel.uiState.value.attachments.size) + assertEquals(uuid1.toString(), viewmodel.uiState.value.attachments[0].workerId) + assertEquals(uuid2.toString(), viewmodel.uiState.value.attachments[1].workerId) + + // Both attachments should be UPLOADING + assertEquals(AttachmentStatus.UPLOADING, viewmodel.uiState.value.attachments[0].status) + assertEquals(AttachmentStatus.UPLOADING, viewmodel.uiState.value.attachments[1].status) + } + @Test fun `Download attachment on selection`() { val fileDownloader: FileDownloader = mockk(relaxed = true) From fee966758e56105cb47cc728780277c5a7ce17af Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:19:47 +0200 Subject: [PATCH 23/94] Add GitHub Action for unit test coverage delta tracking (#3282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add GitHub Action for unit test coverage delta tracking This workflow runs unit tests on PRs and calculates code coverage delta compared to the master branch. It posts a sticky comment on the PR showing coverage changes for student, teacher, and pandautils modules. Key features: - Runs unit tests only (not UI tests) - Calculates instruction coverage percentage delta (not test count) - Updates a single sticky comment on each run to avoid spam - Supports optional threshold check to fail PR if coverage drops 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix open source build and add parallelization - Add missing pendoAccessToken property to open_source_data for student, teacher, and parent apps - Add open_source.sh setup step to both PR and master test jobs - Parallelize workflow: split into 3 jobs (test-pr, test-master, coverage-report) that run PR and master tests concurrently - Add --parallel flag to Gradle test commands - Use artifacts to pass coverage data between jobs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Add parent app and fix gradle paths - Add parent app to coverage tracking (student, teacher, parent, pandautils) - Fix gradle/gradlew path issues by using -p flag consistently - Remove 'cd' commands that broke paths - Rebase on master to include pendoAccessToken fix 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix pandautils test execution path Run pandautils tests from apps build instead of trying to use non-existent libs build. Pandautils is a subproject of apps. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Add parent app to JaCoCo coverage configuration Include parent app in coverage tracking alongside student, teacher, and pandautils. Parent uses the qaDebug variant like student/teacher. Tested locally: - Tests run successfully - JaCoCo report generates correctly - CSV file created at expected location 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Temporarily remove parent from coverage workflow Parent coverage will be added in a follow-up PR after jacoco config is merged to master. This avoids N/A coverage on master branch. --------- Co-authored-by: Claude --- .github/workflows/unit-test-coverage.yml | 258 +++++++++++++++++++++++ gradle/jacoco.gradle | 2 +- 2 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/unit-test-coverage.yml diff --git a/.github/workflows/unit-test-coverage.yml b/.github/workflows/unit-test-coverage.yml new file mode 100644 index 0000000000..e96fb58f69 --- /dev/null +++ b/.github/workflows/unit-test-coverage.yml @@ -0,0 +1,258 @@ +name: Unit Test Coverage + +on: + pull_request: + branches: [ master ] + +jobs: + test-pr: + runs-on: ubuntu-latest + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Setup open source build + run: ./open_source.sh + + - name: Run unit tests and generate coverage for PR + run: | + # Run tests in parallel - Gradle will handle parallelization + ./gradle/gradlew -p apps :student:testQaDebugUnitTest :teacher:testQaDebugUnitTest :pandautils:testDebugUnitTest --parallel + + # Copy exec files to expected locations for jacoco.gradle + mkdir -p apps/student/build/jacoco apps/teacher/build/jacoco libs/pandautils/build/jacoco + + cp apps/student/build/outputs/unit_test_code_coverage/qaDebugUnitTest/testQaDebugUnitTest.exec apps/student/build/jacoco/testQaDebugUnitTest.exec 2>/dev/null || echo "Student exec not found" + cp apps/teacher/build/outputs/unit_test_code_coverage/qaDebugUnitTest/testQaDebugUnitTest.exec apps/teacher/build/jacoco/testQaDebugUnitTest.exec 2>/dev/null || echo "Teacher exec not found" + cp libs/pandautils/build/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec libs/pandautils/build/jacoco/testDebugUnitTest.exec 2>/dev/null || echo "Pandautils exec not found" + + # Generate JaCoCo reports in parallel + ./gradle/gradlew -p apps :student:jacocoReport :teacher:jacocoReport :pandautils:jacocoReport --parallel + continue-on-error: false + + - name: Upload PR coverage reports + uses: actions/upload-artifact@v4 + with: + name: pr-coverage + path: | + apps/student/build/reports/jacoco/jacocoReport/jacocoReport.csv + apps/teacher/build/reports/jacoco/jacocoReport/jacocoReport.csv + libs/pandautils/build/reports/jacoco/jacocoReport/jacocoReport.csv + retention-days: 1 + + test-master: + runs-on: ubuntu-latest + + steps: + - name: Checkout master branch + uses: actions/checkout@v4 + with: + ref: master + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Setup open source build + run: ./open_source.sh + + - name: Run unit tests and generate coverage for master + run: | + # Run tests in parallel - Gradle will handle parallelization + ./gradle/gradlew -p apps :student:testQaDebugUnitTest :teacher:testQaDebugUnitTest :pandautils:testDebugUnitTest --parallel + + # Copy exec files to expected locations for jacoco.gradle + mkdir -p apps/student/build/jacoco apps/teacher/build/jacoco libs/pandautils/build/jacoco + + cp apps/student/build/outputs/unit_test_code_coverage/qaDebugUnitTest/testQaDebugUnitTest.exec apps/student/build/jacoco/testQaDebugUnitTest.exec 2>/dev/null || echo "Student exec not found" + cp apps/teacher/build/outputs/unit_test_code_coverage/qaDebugUnitTest/testQaDebugUnitTest.exec apps/teacher/build/jacoco/testQaDebugUnitTest.exec 2>/dev/null || echo "Teacher exec not found" + cp libs/pandautils/build/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec libs/pandautils/build/jacoco/testDebugUnitTest.exec 2>/dev/null || echo "Pandautils exec not found" + + # Generate JaCoCo reports in parallel + ./gradle/gradlew -p apps :student:jacocoReport :teacher:jacocoReport :pandautils:jacocoReport --parallel + continue-on-error: true + + - name: Upload master coverage reports + uses: actions/upload-artifact@v4 + with: + name: master-coverage + path: | + apps/student/build/reports/jacoco/jacocoReport/jacocoReport.csv + apps/teacher/build/reports/jacoco/jacocoReport/jacocoReport.csv + libs/pandautils/build/reports/jacoco/jacocoReport/jacocoReport.csv + retention-days: 1 + + coverage-report: + runs-on: ubuntu-latest + needs: [test-pr, test-master] + if: always() + + steps: + - name: Download PR coverage + uses: actions/download-artifact@v4 + with: + name: pr-coverage + path: coverage-reports/pr + + - name: Download master coverage + uses: actions/download-artifact@v4 + with: + name: master-coverage + path: coverage-reports/master + continue-on-error: true + + - name: Reorganize coverage files + run: | + # Reorganize downloaded artifacts to expected structure + mkdir -p coverage-reports/pr coverage-reports/master + + # PR coverage + find coverage-reports/pr -name "jacocoReport.csv" -path "*/student/*" -exec cp {} coverage-reports/pr/student.csv \; 2>/dev/null || echo "Student PR coverage not found" + find coverage-reports/pr -name "jacocoReport.csv" -path "*/teacher/*" -exec cp {} coverage-reports/pr/teacher.csv \; 2>/dev/null || echo "Teacher PR coverage not found" + find coverage-reports/pr -name "jacocoReport.csv" -path "*/pandautils/*" -exec cp {} coverage-reports/pr/pandautils.csv \; 2>/dev/null || echo "Pandautils PR coverage not found" + + # Master coverage + find coverage-reports/master -name "jacocoReport.csv" -path "*/student/*" -exec cp {} coverage-reports/master/student.csv \; 2>/dev/null || echo "Student master coverage not found" + find coverage-reports/master -name "jacocoReport.csv" -path "*/teacher/*" -exec cp {} coverage-reports/master/teacher.csv \; 2>/dev/null || echo "Teacher master coverage not found" + find coverage-reports/master -name "jacocoReport.csv" -path "*/pandautils/*" -exec cp {} coverage-reports/master/pandautils.csv \; 2>/dev/null || echo "Pandautils master coverage not found" + + - name: Calculate coverage delta + id: coverage + run: | + python3 << 'EOF' | tee coverage-report.txt + import csv + import os + from pathlib import Path + + def parse_jacoco_csv(file_path): + """Parse JaCoCo CSV and return instruction coverage percentage""" + if not Path(file_path).exists(): + return None + + total_missed = 0 + total_covered = 0 + + with open(file_path, 'r') as f: + reader = csv.DictReader(f) + for row in reader: + total_missed += int(row['INSTRUCTION_MISSED']) + total_covered += int(row['INSTRUCTION_COVERED']) + + if total_missed + total_covered == 0: + return 0.0 + + return (total_covered / (total_missed + total_covered)) * 100 + + modules = ['student', 'teacher', 'pandautils'] + results = [] + + print("## 📊 Code Coverage Report\n") + + overall_pr_coverage = [] + overall_master_coverage = [] + + for module in modules: + pr_file = f'coverage-reports/pr/{module}.csv' + master_file = f'coverage-reports/master/{module}.csv' + + pr_cov = parse_jacoco_csv(pr_file) + master_cov = parse_jacoco_csv(master_file) + + if pr_cov is not None and master_cov is not None: + delta = pr_cov - master_cov + emoji = '✅' if delta >= 0 else '⚠️' + sign = '+' if delta >= 0 else '' + + print(f"### {emoji} {module.capitalize()}") + print(f"- **PR Coverage:** {pr_cov:.2f}%") + print(f"- **Master Coverage:** {master_cov:.2f}%") + print(f"- **Delta:** {sign}{delta:.2f}%\n") + + overall_pr_coverage.append(pr_cov) + overall_master_coverage.append(master_cov) + elif pr_cov is not None: + print(f"### ℹ️ {module.capitalize()}") + print(f"- **PR Coverage:** {pr_cov:.2f}%") + print(f"- **Master Coverage:** N/A\n") + else: + print(f"### ⚠️ {module.capitalize()}") + print(f"- Coverage data not available\n") + + if overall_pr_coverage and overall_master_coverage: + avg_pr = sum(overall_pr_coverage) / len(overall_pr_coverage) + avg_master = sum(overall_master_coverage) / len(overall_master_coverage) + overall_delta = avg_pr - avg_master + + print("---") + print(f"### 📈 Overall Average") + print(f"- **PR Coverage:** {avg_pr:.2f}%") + print(f"- **Master Coverage:** {avg_master:.2f}%") + sign = '+' if overall_delta >= 0 else '' + print(f"- **Delta:** {sign}{overall_delta:.2f}%") + + # Set output for potential failure condition + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f"delta={overall_delta}\n") + + EOF + + - name: Comment PR (sticky) + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('coverage-report.txt', 'utf8'); + const marker = ''; + const body = marker + '\n' + output; + + // Find existing coverage comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existingComment = comments.find(comment => + comment.body.includes(marker) + ); + + if (existingComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: body + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); + } + + # Optional: Fail if coverage decreases by more than 1% + # - name: Check coverage threshold + # if: steps.coverage.outputs.delta < -1.0 + # run: | + # echo "Coverage decreased by more than 1%" + # exit 1 \ No newline at end of file diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index da2c33a4fc..ca8edd7eba 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -def coveredProject = subprojects.findAll { project -> project.name == 'student' || project.name == 'teacher' || project.name == 'pandautils'} +def coveredProject = subprojects.findAll { project -> project.name == 'student' || project.name == 'teacher' || project.name == 'parent' || project.name == 'pandautils'} apply plugin: 'jacoco' From 4a3a4280f15d7fdcca3d7df1f2de30f95ca7951b Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:29:38 +0200 Subject: [PATCH 24/94] [MBL-19271][Teacher] Fix syllabus embedded images not displaying in Edit Syllabus page (#3286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs: MBL-19271 affects: Teacher release note: Fixed an issue where embedded images in syllabus content would not display when editing the syllabus test plan: - Open Teacher app - Navigate to a course with a syllabus containing embedded images - Tap "Edit" on the syllabus page - Verify that embedded images display correctly in the editor 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- .../src/main/java/instructure/rceditor/RCETextEditor.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditor.kt b/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditor.kt index 1faf182a20..336992a1ed 100644 --- a/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditor.kt +++ b/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditor.kt @@ -24,6 +24,7 @@ import android.util.AttributeSet import android.view.MotionEvent import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection +import android.webkit.CookieManager import android.webkit.ValueCallback import android.webkit.WebView import androidx.annotation.RestrictTo @@ -37,6 +38,8 @@ class RCETextEditor @JvmOverloads constructor( init { setEditorBackgroundColor(context.getColor(R.color.rce_backgroundColor)) setEditorFontColor(context.getColor(R.color.rce_defaultTextColor)) + CookieManager.getInstance().setAcceptCookie(true) + CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) } var disallowInterceptTouchEvents: Boolean = true From 93f7ca1ee6d3afd3fa0b628c5982d76822f5f69b Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:02:15 +0200 Subject: [PATCH 25/94] [MBL-19379][Parent] Fixed discussion attachment not downloading. #3287 refs: MBL-19379 affects: Parent release note: Fixed a bug where discussion attachments couldn't be downloaded. --- .../parentapp/di/feature/AssignmentDetailsModule.kt | 6 ++++-- .../details/ParentAssignmentDetailsRouter.kt | 13 ++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AssignmentDetailsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AssignmentDetailsModule.kt index a9e4557dd2..9944ea93e5 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AssignmentDetailsModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AssignmentDetailsModule.kt @@ -29,6 +29,7 @@ import com.instructure.pandautils.features.assignments.details.AssignmentDetails import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter import com.instructure.pandautils.features.assignments.details.AssignmentDetailsSubmissionHandler import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.FileDownloader import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsBehaviour import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsColorProvider import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsRepository @@ -50,9 +51,10 @@ class AssignmentDetailsFragmentModule { navigation: Navigation, parentPrefs: ParentPrefs, apiPrefs: ApiPrefs, - analytics: Analytics + analytics: Analytics, + fileDownloader: FileDownloader ): AssignmentDetailsRouter { - return ParentAssignmentDetailsRouter(navigation, parentPrefs, apiPrefs, analytics) + return ParentAssignmentDetailsRouter(navigation, parentPrefs, apiPrefs, analytics, fileDownloader) } @Provides diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRouter.kt index 5b526f640e..72389da24c 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRouter.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRouter.kt @@ -17,11 +17,13 @@ package com.instructure.parentapp.features.assignment.details import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.RemoteFile import com.instructure.canvasapi2.utils.Analytics import com.instructure.canvasapi2.utils.AnalyticsEventConstants import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions +import com.instructure.pandautils.utils.FileDownloader import com.instructure.parentapp.R import com.instructure.parentapp.util.ParentPrefs import com.instructure.parentapp.util.navigation.Navigation @@ -30,7 +32,8 @@ class ParentAssignmentDetailsRouter( private val navigation: Navigation, private val parentPrefs: ParentPrefs, private val apiPrefs: ApiPrefs, - private val analytics: Analytics + private val analytics: Analytics, + private val fileDownloader: FileDownloader ) : AssignmentDetailsRouter() { override fun navigateToSendMessage(activity: FragmentActivity, options: InboxComposeOptions) { val route = navigation.inboxComposeRoute(options) @@ -66,4 +69,12 @@ class ParentAssignmentDetailsRouter( ) ) } + + override fun navigateToDiscussionAttachmentScreen( + activity: FragmentActivity, + canvasContext: CanvasContext, + attachment: RemoteFile + ) { + fileDownloader.downloadFileToDevice(attachment.url, attachment.fileName, attachment.contentType) + } } From af06dc6a49553637950dc3eda02ad1167c83b933 Mon Sep 17 00:00:00 2001 From: domonkosadam <53480952+domonkosadam@users.noreply.github.com> Date: Wed, 8 Oct 2025 09:12:51 +0200 Subject: [PATCH 26/94] [Horizon][HackDay] Generate Horizon feature tests (#3284) --- libs/horizon/CLAUDE.md | 145 ++++++++ libs/horizon/build.gradle.kts | 62 ++++ .../home/HorizonHomeInteractionTest.kt | 81 +++++ .../horizon/pages/HorizonHomePage.kt | 63 ++++ .../horizon/pages/HorizonInboxPage.kt | 51 +++ .../horizon/pages/HorizonLearnPage.kt | 49 +++ .../pages/HorizonModuleItemSequencePage.kt | 60 ++++ .../horizon/ui/HiltTestActivity.kt | 23 ++ .../ui/features/home/HorizonHomeUiTest.kt | 46 +++ .../inbox/HorizonInboxComposeUiTest.kt | 232 +++++++++++++ .../inbox/HorizonInboxDetailsUiTest.kt | 136 ++++++++ .../features/inbox/HorizonInboxListUiTest.kt | 55 +++ .../ui/features/learn/HorizonLearnUiTest.kt | 55 +++ .../moduleitemsequence/AssessmentUiTest.kt | 153 ++++++++ .../compose/HorizonInboxComposeScreen.kt | 10 +- .../assessment/AssessmentContentScreen.kt | 8 +- .../assignment/comments/CommentsViewModel.kt | 2 +- libs/horizon/src/main/res/values/strings.xml | 2 + .../features/account/AccountRepositoryTest.kt | 76 ++++ .../features/account/AccountViewModelTest.kt | 210 +++++++++++ .../chat/AiAssistChatRepositoryTest.kt | 131 +++++++ .../chat/AiAssistChatViewModelTest.kt | 231 +++++++++++++ .../features/home/HomeRepositoryTest.kt | 111 ++++++ .../features/home/HomeViewModelTest.kt | 178 ++++++++++ ...rizonInboxAttachmentPickerViewModelTest.kt | 196 +++++++++++ .../HorizonInboxComposeRepositoryTest.kt | 221 ++++++++++++ .../HorizonInboxComposeViewModelTest.kt | 282 +++++++++++++++ .../HorizonInboxDetailsRepositoryTest.kt | 271 +++++++++++++++ .../HorizonInboxDetailsViewModelTest.kt | 307 ++++++++++++++++ .../list/HorizonInboxListRepositoryTest.kt | 165 +++++++++ .../list/HorizonInboxListViewModelTest.kt | 156 +++++++++ .../features/learn/LearnRepositoryTest.kt | 126 +++++++ .../features/learn/LearnViewModelTest.kt | 156 +++++++++ .../ModuleItemSequenceRepositoryTest.kt | 192 ++++++++++ .../ModuleItemSequenceViewModelTest.kt | 149 ++++++++ .../assessment/AssessmentRepositoryTest.kt | 158 +++++++++ .../assessment/AssessmentViewModelTest.kt | 184 ++++++++++ .../AssignmentDetailsRepositoryTest.kt | 137 ++++++++ .../AssignmentDetailsViewModelTest.kt | 267 ++++++++++++++ .../comments/CommentsRepositoryTest.kt | 291 ++++++++++++++++ .../comments/CommentsViewModelTest.kt | 327 ++++++++++++++++++ .../content/page/PageDetailsRepositoryTest.kt | 209 +++++++++++ .../content/page/PageDetailsViewModelTest.kt | 261 ++++++++++++++ .../notebook/NotebookRepositoryTest.kt | 147 ++++++++ .../notebook/NotebookViewModelTest.kt | 202 +++++++++++ 45 files changed, 6571 insertions(+), 3 deletions(-) create mode 100644 libs/horizon/CLAUDE.md create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/home/HorizonHomeInteractionTest.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonHomePage.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonInboxPage.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonLearnPage.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonModuleItemSequencePage.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/ui/HiltTestActivity.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/home/HorizonHomeUiTest.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxComposeUiTest.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxDetailsUiTest.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxListUiTest.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/learn/HorizonLearnUiTest.kt create mode 100644 libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/moduleitemsequence/AssessmentUiTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/inbox/attachment/HorizonInboxAttachmentPickerViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsViewModelTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookRepositoryTest.kt create mode 100644 libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookViewModelTest.kt diff --git a/libs/horizon/CLAUDE.md b/libs/horizon/CLAUDE.md new file mode 100644 index 0000000000..eeb23d6f88 --- /dev/null +++ b/libs/horizon/CLAUDE.md @@ -0,0 +1,145 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +Horizon is an Android library module within the canvas-android monorepo that provides a modern, Jetpack Compose-based learning experience for Canvas Career. It is part of the Instructure Canvas LMS mobile ecosystem and integrates with the Student app. The module is located at `libs/horizon` within the monorepo. + +## Build Commands + +All Gradle commands should be run from the repository root (`canvas-android/`), not from the `libs/horizon` directory. + +### Building +```bash +./gradle/gradlew :libs:horizon:assembleDebug +./gradle/gradlew :libs:horizon:assembleRelease +``` + +### Testing +```bash +# Run unit tests (JUnit + Robolectric) +./gradle/gradlew :libs:horizon:testDebugUnitTest + +# Run instrumented tests (Espresso + Compose UI tests) +./gradle/gradlew :libs:horizon:connectedDebugAndroidTest + +# Run specific test class +./gradle/gradlew :libs:horizon:testDebugUnitTest --tests "com.instructure.horizon.features.dashboard.DashboardViewModelTest" +``` + +### Linting and Code Quality +```bash +./gradle/gradlew :libs:horizon:lintDebug +``` + +## Architecture + +### Tech Stack +- **Language**: Kotlin +- **UI**: Jetpack Compose with Material3 +- **Dependency Injection**: Dagger Hilt +- **Navigation**: Jetpack Navigation Compose with type-safe routes using Kotlin Serialization +- **Async**: Kotlin Coroutines and Flow +- **Testing**: JUnit, Mockk, Robolectric, Espresso, Compose UI Test + +### Project Structure + +``` +src/main/java/com/instructure/horizon/ +├── HorizonActivity.kt # Main entry point, handles navigation and push notifications +├── di/ # Hilt dependency injection modules +├── features/ # Feature modules organized by domain +│ ├── account/ +│ ├── aiassistant/ +│ ├── dashboard/ +│ ├── home/ # Landing screen with navigation +│ ├── inbox/ # Messaging functionality +│ ├── learn/ # Course and program learning content +│ ├── moduleitemsequence/ # Sequential module item navigation +│ ├── notebook/ # Note-taking functionality +│ ├── notification/ # Notification center +│ └── skillspace/ +├── horizonui/ # Design system components +│ ├── HorizonTheme.kt # Theme configuration (forced light mode) +│ ├── foundation/ # Colors, typography, spacing +│ ├── molecules/ # Small reusable components +│ ├── organisms/ # Complex composed components +│ └── animation/ # Navigation transitions +├── model/ # Domain models specific to Horizon +└── navigation/ + └── HorizonNavigation.kt # NavHost and routing configuration +``` + +### Design system + + - The `horizonui` package contains the major UI components + - If possible use these components + - If the design contains a reusable component extend the existing design system + - Local Componenents from Figma design shouldn't be placed here, implement them in the feature's package + + +### Feature Architecture Pattern + +Each feature follows MVVM pattern with a consistent structure: + +``` +features/[feature-name]/ +├── [FeatureName]Screen.kt # Composable UI +├── [FeatureName]ViewModel.kt # Hilt-injected ViewModel with StateFlow +├── [FeatureName]Repository.kt # Data layer, API calls +├── [FeatureName]UiState.kt # UI state data class +└── navigation/ # Feature-specific navigation graphs +``` + +**Example flow:** +- `HomeScreen` → `HomeViewModel` → `HomeRepository` → Canvas API +- ViewModels expose `StateFlow` collected in Composables +- Repositories use suspend functions and return API models from `canvasapi2` + +### Key Design Patterns + +1. **Navigation**: Type-safe routes using `@Serializable` data classes in `MainNavigationRoute.kt` +2. **Deep Linking**: Deep links configured in NavHost composables map to Canvas LMS URLs +3. **Dependency Injection**: `@HiltViewModel` for ViewModels, constructor injection for repositories +4. **UI State Management**: Immutable data classes with sealed classes for state variants +5. **Testing**: + - Unit tests use Mockk with `UnconfinedTestDispatcher` for coroutines + - UI tests extend `HorizonTest` base class and use Page Object pattern + +## Important Dependencies + +- `:pandautils` - Core shared utilities, base classes, and common Canvas components +- Canvas API 2 - REST API client for Canvas LMS (`com.instructure.canvasapi2`) +- PSPDFKit - PDF annotation and viewing +- AndroidX WorkManager - Background task scheduling for submissions + +## Testing Notes + +- **Unit Tests**: Located in `src/test/`, use Robolectric for Android framework dependencies +- **Instrumented Tests**: Located in `src/androidTest/`, divided into: + - `espresso/` - Test infrastructure and base classes + - `ui/features/` - UI-only tests using Compose Test + - `interaction/features/` - Full integration tests with data seeding + - Every application screen has a test page which contains all of the important assertions and actions. Create and use these Page classes for every new screen. +- **Test Authentication**: Use `tokenLogin()` method from `HorizonTest` base class for instrumented tests +- **Data Seeding**: Available via `:dataseedingapi` module for instrumented tests +- **Mocked Data**: MockCanvas is an object which can contain all the mock data which will be returned by the mock endpoints during testing + +## Horizon-Specific Considerations + +1. **Theme**: Horizon forces light mode in `HorizonActivity.onCreate()` to ensure consistent branding +2. **Navigation Deep Links**: Must include `ApiPrefs.fullDomain` in URI patterns for proper routing +3. **Module Item Sequence**: Central navigation pattern for course content (assignments, quizzes, pages) +4. **Canvas Career**: This module specifically supports Canvas Career View (`ApiPrefs.canvasCareerView = true`) + +## Code Style + +Follow the code style guidelines from the parent repository: +- No inline comments unless specifically requested - code should be self-documenting +- Use Kotlin idioms and best practices +- Prefer immutability and data classes +- Use descriptive naming for functions and variables +- Write tests that mirror existing test patterns in the project +- Match existing file structure and naming conventions when adding new features +- Alwways create a preview composable for the new composables \ No newline at end of file diff --git a/libs/horizon/build.gradle.kts b/libs/horizon/build.gradle.kts index 6da1ed1098..b10db9822a 100644 --- a/libs/horizon/build.gradle.kts +++ b/libs/horizon/build.gradle.kts @@ -6,6 +6,7 @@ plugins { id("kotlin-kapt") id("dagger.hilt.android.plugin") kotlin("plugin.serialization") version "2.1.20" + id("jacoco") } android { @@ -109,4 +110,65 @@ dependencies { /* Pandautils dependencies to provide fake implementations for testing */ androidTestImplementation(Libs.PLAY_IN_APP_UPDATES) androidTestImplementation(Libs.ROOM) +} + +tasks.register("jacocoTestReport") { + dependsOn("testDebugUnitTest") + + reports { + xml.required.set(true) + html.required.set(true) + csv.required.set(false) + } + + val fileFilter = listOf( + "**/R.class", + "**/R$*.class", + "**/BuildConfig.*", + "**/Manifest*.*", + "**/*Test*.*", + "android/**/*.*", + "**/*\$ViewInjector*.*", + "**/*\$ViewBinder*.*", + "**/Lambda$*.class", + "**/Lambda.class", + "**/*Lambda.class", + "**/*Lambda*.class", + "**/*_MembersInjector.class", + "**/Dagger*Component*.*", + "**/*Module_*Factory.class", + "**/di/module/*", + "**/*_Factory*.*", + "**/*Module*.*", + "**/*Dagger*.*", + "**/*Hilt*.*", + "**/hilt_aggregated_deps/**", + "**/*_HiltModules*.*", + "**/*_ComponentTreeDeps*.*", + "**/*_Impl*.*", + "**/*Screen*.*", + "**/*Ui*.*", + "**/*Navigation*.*", + "**/*Activity*.*", + "**/*Fragment*.*", + "**/*Composable*.*", + "**/*Preview*.*", + "**/horizonui/**", + "**/model/**", + "**/navigation/**" + ) + + val debugTree = fileTree("${layout.buildDirectory.get().asFile}/tmp/kotlin-classes/debug") { + exclude(fileFilter) + include("**/features/**/*ViewModel*.class") + include("**/features/**/*Repository*.class") + } + + val mainSrc = "${project.projectDir}/src/main/java" + + sourceDirectories.setFrom(files(mainSrc)) + classDirectories.setFrom(files(debugTree)) + executionData.setFrom(fileTree(layout.buildDirectory.get().asFile) { + include("jacoco/testDebugUnitTest.exec") + }) } \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/home/HorizonHomeInteractionTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/home/HorizonHomeInteractionTest.kt new file mode 100644 index 0000000000..30b07981ab --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/home/HorizonHomeInteractionTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2025 - 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.horizon.interaction.features.home + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetHorizonCourseManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeJourneyApiManager +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvasapi2.di.graphql.GetCoursesModule +import com.instructure.canvasapi2.di.graphql.JourneyApiManagerModule +import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.JourneyApiManager +import com.instructure.horizon.espresso.HorizonTest +import com.instructure.horizon.pages.HorizonHomePage +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import org.junit.Test + +@HiltAndroidTest +@UninstallModules(GetCoursesModule::class, JourneyApiManagerModule::class) +class HorizonHomeInteractionTest : HorizonTest() { + private val fakeGetHorizonCourseManager = FakeGetHorizonCourseManager() + private val fakeJourneyApiManager = FakeJourneyApiManager() + + @BindValue + @JvmField + val journeyApiManager: JourneyApiManager = fakeJourneyApiManager + + @BindValue + @JvmField + val getCoursesManager: HorizonGetCoursesManager = fakeGetHorizonCourseManager + + private val homePage: HorizonHomePage by lazy { HorizonHomePage(composeTestRule) } + + @Test + fun testBottomNavigationAfterLogin() { + val data = MockCanvas.init( + studentCount = 1, + teacherCount = 1, + courseCount = 1 + ) + val student = data.students.first() + val token = data.tokenFor(student)!! + tokenLogin(data.domain, token, student) + + homePage.assertBottomNavigationVisible() + } + + @Test + fun testNavigationBetweenTabs() { + val data = MockCanvas.init( + studentCount = 1, + teacherCount = 1, + courseCount = 1 + ) + val student = data.students.first() + val token = data.tokenFor(student)!! + tokenLogin(data.domain, token, student) + + homePage.assertBottomNavigationVisible() + homePage.clickLearnTab() + homePage.clickHomeTab() + homePage.clickAccountTab() + homePage.clickHomeTab() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonHomePage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonHomePage.kt new file mode 100644 index 0000000000..bf6af8a6ca --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonHomePage.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025 - 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.horizon.pages + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class HorizonHomePage(private val composeTestRule: ComposeTestRule) { + fun assertBottomNavigationVisible() { + composeTestRule.onNodeWithText("Home") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Learn") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("AI assist") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Skillspace") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Account") + .assertIsDisplayed() + } + + fun clickHomeTab() { + composeTestRule.onNodeWithText("Home") + .performClick() + } + + fun clickLearnTab() { + composeTestRule.onNodeWithText("Learn") + .performClick() + } + + fun clickAiAssistantTab() { + composeTestRule.onNodeWithContentDescription("AI assist") + .performClick() + } + + fun clickSkillspaceTab() { + composeTestRule.onNodeWithText("Skillspace") + .performClick() + } + + fun clickAccountTab() { + composeTestRule.onNodeWithText("Account") + .performClick() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonInboxPage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonInboxPage.kt new file mode 100644 index 0000000000..7d15e29cad --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonInboxPage.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 - 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.horizon.pages + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class HorizonInboxPage(private val composeTestRule: ComposeTestRule) { + fun assertInboxItemDisplayed(subject: String) { + composeTestRule.onNodeWithText(subject) + .assertIsDisplayed() + } + + fun clickInboxItem(subject: String) { + composeTestRule.onNodeWithText(subject) + .performClick() + } + + fun clickComposeButton() { + composeTestRule.onNodeWithContentDescription("Compose") + .performClick() + } + + fun assertEmptyState() { + composeTestRule.onNode(hasText("No messages", substring = true)) + .assertIsDisplayed() + } + + fun assertConversationCount(count: Int) { + composeTestRule.onNodeWithText("$count conversations", substring = true) + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonLearnPage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonLearnPage.kt new file mode 100644 index 0000000000..2acbc847d2 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonLearnPage.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025 - 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.horizon.pages + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class HorizonLearnPage(private val composeTestRule: ComposeTestRule) { + fun assertCourseDisplayed(courseName: String) { + composeTestRule.onNodeWithText(courseName) + .assertIsDisplayed() + } + + fun clickCourse(courseName: String) { + composeTestRule.onNodeWithText(courseName) + .performClick() + } + + fun assertProgramDisplayed(programName: String) { + composeTestRule.onNodeWithText(programName) + .assertIsDisplayed() + } + + fun clickProgram(programName: String) { + composeTestRule.onNodeWithText(programName) + .performClick() + } + + fun assertEmptyState() { + composeTestRule.onNodeWithText("No courses", substring = true) + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonModuleItemSequencePage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonModuleItemSequencePage.kt new file mode 100644 index 0000000000..fa5a3d076b --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonModuleItemSequencePage.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 - 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.horizon.pages + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class HorizonModuleItemSequencePage(private val composeTestRule: ComposeTestRule) { + fun assertModuleItemDisplayed(title: String) { + composeTestRule.onNodeWithText(title) + .assertIsDisplayed() + } + + fun clickNextButton() { + composeTestRule.onNodeWithContentDescription("Next") + .performClick() + } + + fun clickPreviousButton() { + composeTestRule.onNodeWithContentDescription("Previous") + .performClick() + } + + fun clickProgressButton() { + composeTestRule.onNodeWithContentDescription("Progress") + .performClick() + } + + fun assertProgressDisplayed() { + composeTestRule.onNodeWithText("Progress", substring = true) + .assertIsDisplayed() + } + + fun clickMarkAsDone() { + composeTestRule.onNodeWithText("Mark as done") + .performClick() + } + + fun assertMarkedAsDone() { + composeTestRule.onNodeWithText("Marked as done", substring = true) + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/HiltTestActivity.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/HiltTestActivity.kt new file mode 100644 index 0000000000..8e60d0c5ef --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/HiltTestActivity.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 - 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.horizon.ui + +import androidx.activity.ComponentActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class HiltTestActivity : ComponentActivity() diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/home/HorizonHomeUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/home/HorizonHomeUiTest.kt new file mode 100644 index 0000000000..38b5a35024 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/home/HorizonHomeUiTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 - 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.horizon.ui.features.home + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.horizonui.molecules.Spinner +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@HiltAndroidTest +@RunWith(AndroidJUnit4::class) +class HorizonHomeUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testLoadingStateDisplaysSpinner() { + composeTestRule.setContent { + Spinner(modifier = Modifier.fillMaxSize()) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxComposeUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxComposeUiTest.kt new file mode 100644 index 0000000000..c049dfdff0 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxComposeUiTest.kt @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2025 - 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.horizon.ui.features.inbox + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.text.input.TextFieldValue +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Recipient +import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachmentPickerUiState +import com.instructure.horizon.features.inbox.compose.HorizonInboxComposeScreen +import com.instructure.horizon.features.inbox.compose.HorizonInboxComposeUiState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class HorizonInboxComposeUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val pickerState = HorizonInboxAttachmentPickerUiState() + + @Test + fun testComposeScreenDisplaysCourseSelector() { + val uiState = HorizonInboxComposeUiState( + coursePickerOptions = listOf( + Course(id = 1L, name = "Math 101"), + Course(id = 2L, name = "Science 202") + ) + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithContentDescription("Select Course") + .assertIsDisplayed() + } + + @Test + fun testComposeScreenDisplaysRecipientField() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Recipient(s)") + .assertIsDisplayed() + } + + @Test + fun testComposeScreenDisplaysSubjectField() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Title/Subject") + .assertIsDisplayed() + } + + @Test + fun testComposeScreenDisplaysMessageField() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Message") + .assertIsDisplayed() + } + + @Test + fun testSendButtonDisabledWhenFieldsEmpty() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Send") + .assertIsDisplayed() + .assertHasClickAction() + } + + @Test + fun testSelectedCourseDisplays() { + val uiState = HorizonInboxComposeUiState( + selectedCourse = Course(id = 1L, name = "Math 101") + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Math 101") + .assertIsDisplayed() + } + + @Test + fun testSelectedRecipientsDisplay() { + val uiState = HorizonInboxComposeUiState( + selectedRecipients = listOf( + Recipient(stringId = "1", name = "John Doe"), + Recipient(stringId = "2", name = "Jane Smith") + ) + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("John Doe") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Jane Smith") + .assertIsDisplayed() + } + + @Test + fun testSubjectTextDisplays() { + val uiState = HorizonInboxComposeUiState( + subject = TextFieldValue("Test Subject") + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Test Subject") + .assertIsDisplayed() + } + + @Test + fun testBodyTextDisplays() { + val uiState = HorizonInboxComposeUiState( + body = TextFieldValue("Test message body") + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Test message body") + .assertIsDisplayed() + } + + @Test + fun testSendIndividuallyCheckboxDisplays() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Send an individual message to each recipient") + .assertIsDisplayed() + } + + @Test + fun testErrorMessagesDisplay() { + val uiState = HorizonInboxComposeUiState( + courseErrorMessage = "Please select a course", + recipientErrorMessage = "Please select at least one recipient", + subjectErrorMessage = "Subject is required", + bodyErrorMessage = "Message is required" + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Please select a course") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Please select at least one recipient") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Subject is required") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Message is required") + .assertIsDisplayed() + } + + @Test + fun testAttachmentButtonDisplays() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Attach file") + .assertIsDisplayed() + .assertHasClickAction() + } + + @Test + fun testLoadingStateDisplaysProgressIndicator() { + val uiState = HorizonInboxComposeUiState( + isSendLoading = true + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxDetailsUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxDetailsUiTest.kt new file mode 100644 index 0000000000..2f6815a6a1 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxDetailsUiTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2025 - 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.horizon.ui.features.inbox + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.features.inbox.details.HorizonInboxDetailsItem +import com.instructure.horizon.features.inbox.details.HorizonInboxDetailsScreen +import com.instructure.horizon.features.inbox.details.HorizonInboxDetailsUiState +import com.instructure.horizon.horizonui.platform.LoadingState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class HorizonInboxDetailsUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testDetailsScreenDisplaysTitle() { + val uiState = HorizonInboxDetailsUiState( + title = "Test Conversation", + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + HorizonInboxDetailsScreen(uiState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Test Conversation") + .assertIsDisplayed() + } + + @Test + fun testDetailsScreenDisplaysMessages() { + val uiState = HorizonInboxDetailsUiState( + title = "Conversation", + items = listOf( + HorizonInboxDetailsItem( + author = "John Doe", + date = Date(), + isHtmlContent = false, + content = "Test message content", + attachments = emptyList() + ) + ), + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + HorizonInboxDetailsScreen(uiState, rememberNavController()) + } + + composeTestRule.onNodeWithText("John Doe") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Test message content") + .assertIsDisplayed() + } + + @Test + fun testLoadingStateDisplaysSpinner() { + val uiState = HorizonInboxDetailsUiState( + title = "Loading...", + loadingState = LoadingState(isLoading = true) + ) + + composeTestRule.setContent { + HorizonInboxDetailsScreen(uiState, rememberNavController()) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testMultipleMessagesDisplay() { + val uiState = HorizonInboxDetailsUiState( + title = "Conversation", + items = listOf( + HorizonInboxDetailsItem( + author = "Student 1", + date = Date(), + isHtmlContent = false, + content = "First message", + attachments = emptyList() + ), + HorizonInboxDetailsItem( + author = "Teacher", + date = Date(), + isHtmlContent = false, + content = "Second message", + attachments = emptyList() + ), + HorizonInboxDetailsItem( + author = "Student 1", + date = Date(), + isHtmlContent = false, + content = "Third message", + attachments = emptyList() + ) + ), + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + HorizonInboxDetailsScreen(uiState, rememberNavController()) + } + + composeTestRule.onNodeWithText("First message") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Second message") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Third message") + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxListUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxListUiTest.kt new file mode 100644 index 0000000000..bf046a12f4 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxListUiTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2025 - 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.horizon.ui.features.inbox + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +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.horizon.horizonui.molecules.Spinner +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class HorizonInboxListUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testLoadingStateDisplaysSpinner() { + composeTestRule.setContent { + Spinner(modifier = Modifier.fillMaxSize()) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testInboxTitleDisplays() { + composeTestRule.setContent { + androidx.compose.material3.Text("Inbox") + } + + composeTestRule.onNodeWithText("Inbox") + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/learn/HorizonLearnUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/learn/HorizonLearnUiTest.kt new file mode 100644 index 0000000000..88d04e3d0d --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/learn/HorizonLearnUiTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2025 - 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.horizon.ui.features.learn + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +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.horizon.horizonui.molecules.Spinner +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class HorizonLearnUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testLoadingStateDisplaysSpinner() { + composeTestRule.setContent { + Spinner(modifier = Modifier.fillMaxSize()) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testLearnTitleDisplays() { + composeTestRule.setContent { + androidx.compose.material3.Text("Learn") + } + + composeTestRule.onNodeWithText("Learn") + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/moduleitemsequence/AssessmentUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/moduleitemsequence/AssessmentUiTest.kt new file mode 100644 index 0000000000..ca16cfe0ee --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/moduleitemsequence/AssessmentUiTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2025 - 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.horizon.ui.features.moduleitemsequence + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.features.moduleitemsequence.content.assessment.AssessmentContentScreen +import com.instructure.horizon.features.moduleitemsequence.content.assessment.AssessmentUiState +import com.instructure.horizon.horizonui.platform.LoadingState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AssessmentUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testStartQuizButtonDisplays() { + val uiState = AssessmentUiState( + assessmentName = "Math Quiz", + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithText("Start quiz") + .assertIsDisplayed() + .assertHasClickAction() + } + + @Test + fun testAssessmentNameDisplays() { + val uiState = AssessmentUiState( + assessmentName = "Final Exam", + showAssessmentDialog = true, + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithText("Final Exam") + .assertIsDisplayed() + } + + @Test + fun testLoadingStateDisplaysSpinner() { + val uiState = AssessmentUiState( + assessmentName = "Quiz", + loadingState = LoadingState(isLoading = true) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testAssessmentDialogDisplaysWhenOpen() { + val uiState = AssessmentUiState( + assessmentName = "Quiz", + showAssessmentDialog = true, + urlToLoad = "https://example.com/quiz", + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithTag("AssessmentDialog") + .assertIsDisplayed() + } + + @Test + fun testAssessmentLoadingIndicatorDisplays() { + val uiState = AssessmentUiState( + assessmentName = "Quiz", + showAssessmentDialog = true, + assessmentLoading = true, + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testCompletionLoadingDisplays() { + val uiState = AssessmentUiState( + assessmentName = "Quiz", + assessmentCompletionLoading = true, + showAssessmentDialog = true, + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testCloseAssessmentButtonDisplays() { + val uiState = AssessmentUiState( + assessmentName = "Quiz", + showAssessmentDialog = true, + urlToLoad = "https://example.com/quiz", + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithContentDescription("Close") + .assertIsDisplayed() + .assertHasClickAction() + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt index f75aecc6ca..7680fca08e 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt @@ -47,6 +47,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -275,7 +277,13 @@ private fun CourseRecipientPickerSection(state: HorizonInboxComposeUiState) { onMenuOpenChanged = { isRecipientPickerOpened = it }, minSearchQueryLengthForMenu = state.minQueryLength ) - MultiSelectSearch(recipientPickerState) + val context = LocalContext.current + MultiSelectSearch( + recipientPickerState, + Modifier.semantics { + contentDescription = context.getString(R.string.a11y_inboxComposeSelectCourse) + } + ) HorizonSpace(SpaceSize.SPACE_12) } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentContentScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentContentScreen.kt index b0aa28016c..f098a41a05 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentContentScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentContentScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -112,7 +113,10 @@ fun AssessmentContentScreen( modifier = Modifier .fillMaxSize() .padding(top = 16.dp) - .background(HorizonColors.Surface.pageSecondary(), shape = HorizonCornerRadius.level5) + .background( + HorizonColors.Surface.pageSecondary(), + shape = HorizonCornerRadius.level5 + ).testTag("AssessmentDialog") ) { Box( modifier = Modifier @@ -136,8 +140,10 @@ fun AssessmentContentScreen( progress = null ) } else { + val context = LocalContext.current IconButton( iconRes = R.drawable.close, + contentDescription = context.getString(R.string.a11y_close), color = IconButtonColor.Inverse, modifier = Modifier .align(Alignment.CenterEnd), diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModel.kt index 0bc6a6b475..e53c8e25e9 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModel.kt @@ -200,10 +200,10 @@ class CommentsViewModel @Inject constructor( viewModelScope.tryLaunch { commentsRepository.postComment(courseId, assignmentId, apiPrefs.user?.id.orDefault(), attempt, commentText).dataOrThrow - reloadData() _uiState.update { it.copy(comment = TextFieldValue(""), postingComment = false) } + reloadData() } catch { _ -> _uiState.update { it.copy(postingComment = false, errorMessage = context.getString(R.string.commentsBottomSheet_failedToPostComment)) } } diff --git a/libs/horizon/src/main/res/values/strings.xml b/libs/horizon/src/main/res/values/strings.xml index d3fac667bb..d17872d8b6 100644 --- a/libs/horizon/src/main/res/values/strings.xml +++ b/libs/horizon/src/main/res/values/strings.xml @@ -368,4 +368,6 @@ Welcome! View your program to enroll in your first course. Program details Congrats! You’ve completed your course. View your progress and scores on the Learn page. + Select Course + Close \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountRepositoryTest.kt new file mode 100644 index 0000000000..0854a642e0 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountRepositoryTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.account + +import com.instructure.canvasapi2.apis.ExperienceAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.models.ExperienceSummary +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class AccountRepositoryTest { + private val userApi: UserAPI.UsersInterface = mockk(relaxed = true) + private val experienceAPI: ExperienceAPI = mockk(relaxed = true) + + @Test + fun `Test successful user details retrieval`() = runTest { + val user = User(id = 1L, name = "Test User", email = "test@example.com") + coEvery { userApi.getSelf(any()) } returns DataResult.Success(user) + + val result = getRepository().getUserDetails(false) + + assertEquals(user, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed user details retrieval throws exception`() = runTest { + coEvery { userApi.getSelf(any()) } returns DataResult.Fail() + + getRepository().getUserDetails(false) + } + + @Test + fun `Test successful experiences retrieval`() = runTest { + val experiences = ExperienceSummary(availableApps = listOf("app1", "app2")) + coEvery { experienceAPI.getExperienceSummary(any()) } returns DataResult.Success(experiences) + + val result = getRepository().getExperiences(false) + + assertEquals(2, result.size) + assertTrue(result.contains("app1")) + assertTrue(result.contains("app2")) + } + + @Test + fun `Test failed experiences retrieval returns empty list`() = runTest { + coEvery { experienceAPI.getExperienceSummary(any()) } returns DataResult.Fail() + + val result = getRepository().getExperiences(false) + + assertTrue(result.isEmpty()) + } + + private fun getRepository(): AccountRepository { + return AccountRepository(userApi, experienceAPI) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountViewModelTest.kt new file mode 100644 index 0000000000..133e2b423b --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountViewModelTest.kt @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.account + +import android.content.Context +import com.instructure.canvasapi2.models.ExperienceSummary +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.reminder.AlarmScheduler +import com.instructure.pandautils.room.offline.DatabaseProvider +import com.instructure.pandautils.utils.LogoutHelper +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AccountViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: AccountRepository = mockk(relaxed = true) + private val logoutHelper: LogoutHelper = mockk(relaxed = true) + private val databaseProvider: DatabaseProvider = mockk(relaxed = true) + private val alarmScheduler: AlarmScheduler = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testUser = User( + id = 1L, + name = "Test User", + shortName = "TUser" + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getUserDetails(any()) } returns testUser + coEvery { repository.getExperiences(any()) } returns listOf() + every { context.getString(any()) } returns "Test String" + every { context.getString(any(), any()) } returns "Test String" + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads successfully on init`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.screenState.isLoading) + coVerify { repository.getUserDetails(false) } + } + + @Test + fun `Test user name is set from repository`() = runTest { + val viewModel = getViewModel() + + assertEquals(testUser.shortName, viewModel.uiState.value.userName) + } + + @Test + fun `Test user name falls back to name if shortName is null`() = runTest { + val userWithoutShortName = testUser.copy(shortName = null) + coEvery { repository.getUserDetails(any()) } returns userWithoutShortName + + val viewModel = getViewModel() + + assertEquals(testUser.name, viewModel.uiState.value.userName) + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getUserDetails(any()) } throws Exception("Network error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.screenState.isLoading) + assertTrue(viewModel.uiState.value.screenState.isError) + } + + @Test + fun `Test experience switcher is shown when academic experience is available`() = runTest { + coEvery { repository.getExperiences(any()) } returns listOf(ExperienceSummary.ACADEMIC_EXPERIENCE) + + val viewModel = getViewModel() + + val hasExperienceGroup = viewModel.uiState.value.accountGroups.any { group -> + group.items.any { it.type is AccountItemType.SwitchExperience } + } + assertTrue(hasExperienceGroup) + } + + @Test + fun `Test experience switcher is hidden when academic experience is not available`() = runTest { + coEvery { repository.getExperiences(any()) } returns listOf() + + val viewModel = getViewModel() + + val hasExperienceGroup = viewModel.uiState.value.accountGroups.any { group -> + group.items.any { it.type is AccountItemType.SwitchExperience } + } + assertFalse(hasExperienceGroup) + } + + @Test + fun `Test account groups are initialized`() = runTest { + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.accountGroups.isNotEmpty()) + } + + @Test + fun `Test settings group contains expected items`() = runTest { + val viewModel = getViewModel() + + val settingsGroup = viewModel.uiState.value.accountGroups.firstOrNull { group -> + group.items.any { it.type is AccountItemType.Open } + } + + assertTrue(settingsGroup != null) + assertTrue(settingsGroup!!.items.isNotEmpty()) + } + + @Test + fun `Test logout group is present`() = runTest { + val viewModel = getViewModel() + + val logoutGroup = viewModel.uiState.value.accountGroups.firstOrNull { group -> + group.items.any { it.type is AccountItemType.LogOut } + } + + assertTrue(logoutGroup != null) + } + + @Test + fun `Test update user name changes state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateUserName("New Name") + + assertEquals("New Name", viewModel.uiState.value.userName) + } + + @Test + fun `Test perform logout calls logout helper`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.performLogout() + + verify { logoutHelper.logout(databaseProvider, alarmScheduler) } + } + + @Test + fun `Test switch experience sets restart flag`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.switchExperience() + + assertTrue(viewModel.uiState.value.restartApp) + verify { apiPrefs.canvasCareerView = false } + } + + @Test + fun `Test experiences are fetched`() = runTest { + val viewModel = getViewModel() + + coVerify { repository.getExperiences(false) } + } + + private fun getViewModel(): AccountViewModel { + return AccountViewModel( + context, + repository, + logoutHelper, + databaseProvider, + alarmScheduler, + apiPrefs + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatRepositoryTest.kt new file mode 100644 index 0000000000..a09f651b78 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatRepositoryTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.aiassistant.chat + +import com.instructure.canvasapi2.managers.CedarApiManager +import com.instructure.canvasapi2.managers.DocumentSource +import com.instructure.canvasapi2.managers.PineApiManager +import com.instructure.pine.type.MessageInput +import com.instructure.pine.type.Role +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class AiAssistChatRepositoryTest { + private val cedarApi: CedarApiManager = mockk(relaxed = true) + private val pineApi: PineApiManager = mockk(relaxed = true) + + @Test + fun `Test answer prompt without context`() = runTest { + val prompt = "What is 2+2?" + val expectedResponse = "4" + + coEvery { cedarApi.answerPrompt(prompt, null) } returns expectedResponse + + val result = getRepository().answerPrompt(prompt) + + assertEquals(expectedResponse, result) + coVerify { cedarApi.answerPrompt(prompt, null) } + } + + @Test + fun `Test answer prompt with context`() = runTest { + val prompt = "Summarize this content" + val context = "This is test content to summarize" + val expectedResponse = "Summary of test content" + + coEvery { cedarApi.answerPrompt(any(), any()) } returns expectedResponse + + val result = getRepository().answerPrompt(prompt, context) + + assertEquals(expectedResponse, result) + coVerify { cedarApi.answerPrompt(prompt, match { it != null }) } + } + + @Test + fun `Test answer prompt with messages and context map`() = runTest { + val messages = listOf( + MessageInput(role = Role.User, text = "Test message") + ) + val context = mapOf("courseId" to "123") + val expectedResponse = "AI response" + + coEvery { pineApi.queryDocument(messages, DocumentSource.canvas, context) } returns expectedResponse + + val result = getRepository().answerPrompt(messages, context) + + assertEquals(expectedResponse, result) + } + + @Test + fun `Test summarize prompt with default paragraphs`() = runTest { + val contextString = "Long content to summarize..." + val summaryParagraphs = listOf("Summary paragraph 1", "Summary paragraph 2") + val expectedResponse = summaryParagraphs.joinToString("\n") + + coEvery { cedarApi.summarizeContent(contextString, 1) } returns summaryParagraphs + + val result = getRepository().summarizePrompt(contextString) + + assertEquals(expectedResponse, result) + coVerify { cedarApi.summarizeContent(contextString, 1) } + } + + @Test + fun `Test summarize prompt with custom paragraph count`() = runTest { + val contextString = "Long content to summarize..." + val numberOfParagraphs = 3 + val summaryParagraphs = listOf("Para 1", "Para 2", "Para 3") + val expectedResponse = summaryParagraphs.joinToString("\n") + + coEvery { cedarApi.summarizeContent(contextString, numberOfParagraphs) } returns summaryParagraphs + + val result = getRepository().summarizePrompt(contextString, numberOfParagraphs) + + assertEquals(expectedResponse, result) + coVerify { cedarApi.summarizeContent(contextString, numberOfParagraphs) } + } + + @Test + fun `Test empty summary returns empty string`() = runTest { + val contextString = "Content" + coEvery { cedarApi.summarizeContent(contextString, 1) } returns emptyList() + + val result = getRepository().summarizePrompt(contextString) + + assertEquals("", result) + } + + @Test + fun `Test prompt with empty context string`() = runTest { + val prompt = "Test prompt" + val expectedResponse = "Response" + + coEvery { cedarApi.answerPrompt(prompt, null) } returns expectedResponse + + val result = getRepository().answerPrompt(prompt, null) + + assertEquals(expectedResponse, result) + } + + private fun getRepository(): AiAssistChatRepository { + return AiAssistChatRepository(cedarApi, pineApi) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt new file mode 100644 index 0000000000..f56f94a89e --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.aiassistant.chat + +import android.content.Context +import androidx.compose.ui.text.input.TextFieldValue +import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider +import com.instructure.horizon.features.aiassistant.common.model.AiAssistContext +import com.instructure.horizon.features.aiassistant.common.model.AiAssistMessage +import com.instructure.horizon.features.aiassistant.common.model.AiAssistMessagePrompt +import com.instructure.horizon.features.aiassistant.common.model.AiAssistMessageRole +import com.instructure.pine.type.MessageInput +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AiAssistChatViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: AiAssistChatRepository = mockk(relaxed = true) + private val aiAssistContextProvider: AiAssistContextProvider = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testContext = AiAssistContext( + contextString = "Test context", + contextSources = emptyList(), + chatHistory = mutableListOf() + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + every { aiAssistContextProvider.aiAssistContext } returns testContext + coEvery { repository.answerPrompt(any(), any()) } returns "Test response" + coEvery { repository.answerPrompt(any>(), any>()) } returns "Test response" + coEvery { repository.summarizePrompt(any()) } returns "Summary response" + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel initializes with empty chat history`() = runTest { + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.messages.isEmpty()) + assertFalse(viewModel.uiState.value.isLoading) + } + + @Test + fun `Test ViewModel initializes with existing chat history`() = runTest { + val existingMessage = AiAssistMessage( + prompt = AiAssistMessagePrompt.Summarize, + role = AiAssistMessageRole.User + ) + val contextWithHistory = testContext.copy(chatHistory = mutableListOf(existingMessage)) + every { aiAssistContextProvider.aiAssistContext } returns contextWithHistory + + val viewModel = getViewModel() + + assertEquals(2, viewModel.uiState.value.messages.size) + assertFalse(viewModel.uiState.value.isLoading) + } + + @Test + fun `Test text input change updates state`() = runTest { + val viewModel = getViewModel() + + val newText = TextFieldValue("Test input") + viewModel.uiState.value.onInputTextChanged(newText) + + assertEquals("Test input", viewModel.uiState.value.inputTextValue.text) + } + + @Test + fun `Test text submission sends message`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test message")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + assertTrue(viewModel.uiState.value.messages.any { + it.role == AiAssistMessageRole.User && + it.prompt is AiAssistMessagePrompt.Custom && + (it.prompt as AiAssistMessagePrompt.Custom).message == "Test message" + }) + } + + @Test + fun `Test text submission receives response`() = runTest { + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test message")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + assertTrue(viewModel.uiState.value.messages.any { + it.role == AiAssistMessageRole.Assistant + }) + assertFalse(viewModel.uiState.value.isLoading) + } + + @Test + fun `Test text submission clears input field`() = runTest { + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test message")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + assertEquals("", viewModel.uiState.value.inputTextValue.text) + } + + @Test + fun `Test existing summarize prompt is executed`() = runTest { + val existingMessage = AiAssistMessage( + prompt = AiAssistMessagePrompt.Summarize, + role = AiAssistMessageRole.User + ) + val contextWithHistory = testContext.copy(chatHistory = mutableListOf(existingMessage)) + every { aiAssistContextProvider.aiAssistContext } returns contextWithHistory + + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + coVerify { repository.summarizePrompt(any()) } + assertTrue(viewModel.uiState.value.messages.size >= 2) + } + + @Test + fun `Test custom prompt calls answer prompt`() = runTest { + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Custom question")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + coVerify { repository.answerPrompt("Custom question", any()) } + } + + @Test + fun `Test loading state is set during message submission`() = runTest { + coEvery { repository.answerPrompt(any(), any()) } coAnswers { + // Simulate a delay + kotlinx.coroutines.delay(100) + "Response" + } + + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test message")) + viewModel.uiState.value.onInputTextSubmitted() + + // The loading state should be set immediately + assertTrue(viewModel.uiState.value.isLoading) + + testDispatcher.scheduler.advanceUntilIdle() + + // After completion, loading should be false + assertFalse(viewModel.uiState.value.isLoading) + } + + @Test + fun `Test AI context is passed to UI state`() = runTest { + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + assertEquals(testContext, viewModel.uiState.value.aiContext) + } + + @Test + fun `Test messages are appended in correct order`() = runTest { + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("First message")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Second message")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + val messages = viewModel.uiState.value.messages + assertTrue(messages.size >= 4) // 2 user messages + 2 assistant responses + + // Check that messages alternate between user and assistant + val userMessages = messages.filterIndexed { index, _ -> index % 2 == 0 } + assertTrue(userMessages.all { it.role == AiAssistMessageRole.User }) + } + + private fun getViewModel(): AiAssistChatViewModel { + return AiAssistChatViewModel(context, repository, aiAssistContextProvider) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeRepositoryTest.kt new file mode 100644 index 0000000000..98063edb28 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeRepositoryTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.home + +import com.instructure.canvasapi2.apis.ThemeAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.managers.CourseWithProgress +import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.models.CanvasTheme +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class HomeRepositoryTest { + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val themeApi: ThemeAPI.ThemeInterface = mockk(relaxed = true) + private val userApi: UserAPI.UsersInterface = mockk(relaxed = true) + private val getCoursesManager: HorizonGetCoursesManager = mockk(relaxed = true) + + private val userId = 1L + + @Before + fun setup() { + every { apiPrefs.user } returns User(id = userId, name = "Test User") + } + + @Test + fun `Test successful theme retrieval`() = runTest { + val theme = CanvasTheme("", "", "", "", "", "", "", "") + coEvery { themeApi.getTheme(any()) } returns DataResult.Success(theme) + + val result = getRepository().getTheme() + + assertEquals(theme, result) + } + + @Test + fun `Test failed theme retrieval returns null`() = runTest { + coEvery { themeApi.getTheme(any()) } returns DataResult.Fail() + + val result = getRepository().getTheme() + + assertNull(result) + } + + @Test + fun `Test successful user retrieval`() = runTest { + val user = User(id = userId, name = "Test User", locale = "en") + coEvery { userApi.getSelf(any()) } returns DataResult.Success(user) + + val result = getRepository().getSelf() + + assertEquals(user, result) + } + + @Test + fun `Test failed user retrieval returns null`() = runTest { + coEvery { userApi.getSelf(any()) } returns DataResult.Fail() + + val result = getRepository().getSelf() + + assertNull(result) + } + + @Test + fun `Test successful courses retrieval`() = runTest { + val courses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Course 1", courseSyllabus = "", progress = 50.0), + CourseWithProgress(courseId = 2L, courseName = "Course 2", courseSyllabus = "", progress = 75.0) + ) + coEvery { getCoursesManager.getCoursesWithProgress(userId, false) } returns DataResult.Success(courses) + + val result = getRepository().getCourses() + + assertEquals(2, result.size) + assertEquals(courses, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed courses retrieval throws exception`() = runTest { + coEvery { getCoursesManager.getCoursesWithProgress(userId, false) } returns DataResult.Fail() + + getRepository().getCourses() + } + + private fun getRepository(): HomeRepository { + return HomeRepository(apiPrefs, themeApi, userApi, getCoursesManager) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeViewModelTest.kt new file mode 100644 index 0000000000..065fda06c9 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeViewModelTest.kt @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.home + +import android.content.Context +import com.instructure.canvasapi2.managers.CourseWithProgress +import com.instructure.canvasapi2.models.CanvasTheme +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider +import com.instructure.pandautils.utils.LocaleUtils +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class HomeViewModelTest { + private val context: Context = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val homeRepository: HomeRepository = mockk(relaxed = true) + private val localeUtils: LocaleUtils = mockk(relaxed = true) + private val aiAssistContextProvider: AiAssistContextProvider = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testUser = User(id = 1L, name = "Test User", locale = "en") + private val testTheme = CanvasTheme("", "", "", "", "", "", "", "") + private val testCourses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Course 1", courseSyllabus = "", progress = 50.0), + CourseWithProgress(courseId = 2L, courseName = "Course 2", courseSyllabus = "", progress = 75.0) + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + every { apiPrefs.effectiveLocale } returns "en" + coEvery { homeRepository.getSelf() } returns testUser + coEvery { homeRepository.getTheme() } returns testTheme + coEvery { homeRepository.getCourses() } returns testCourses + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test successful data load updates UI state`() = runTest { + val viewModel = getViewModel() + + val state = viewModel.uiState.value + + assertFalse(state.initialDataLoading) + assertEquals(testTheme, state.theme) + verify { apiPrefs.user = testUser } + } + + @Test + fun `Test theme is loaded from repository`() = runTest { + val viewModel = getViewModel() + + coVerify { homeRepository.getTheme() } + assertEquals(testTheme, viewModel.uiState.value.theme) + } + + @Test + fun `Test user is saved to API prefs`() = runTest { + val viewModel = getViewModel() + + verify { apiPrefs.user = testUser } + } + + @Test + fun `Test courses are loaded`() = runTest { + val viewModel = getViewModel() + + coVerify { homeRepository.getCourses() } + } + + @Test + fun `Test locale change triggers app restart`() = runTest { + every { apiPrefs.effectiveLocale } returnsMany listOf("en", "es") + + val viewModel = getViewModel() + + verify { localeUtils.restartApp(context) } + } + + @Test + fun `Test no restart when locale unchanged`() = runTest { + every { apiPrefs.effectiveLocale } returns "en" + + val viewModel = getViewModel() + + verify(exactly = 0) { localeUtils.restartApp(any()) } + } + + @Test + fun `Test failed data load sets loading to false`() = runTest { + coEvery { homeRepository.getSelf() } throws Exception("Network error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.initialDataLoading) + } + + @Test + fun `Test updateShowAiAssist updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateShowAiAssist(true) + + assertTrue(viewModel.uiState.value.showAiAssist) + } + + @Test + fun `Test updateShowAiAssist sets AI context with course IDs`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateShowAiAssist(true) + + verify { aiAssistContextProvider.aiAssistContext = match { it.contextSources.size == 2 } } + } + + @Test + fun `Test null user does not crash`() = runTest { + coEvery { homeRepository.getSelf() } returns null + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.initialDataLoading) + verify(exactly = 0) { apiPrefs.user = any() } + } + + @Test + fun `Test null theme does not crash`() = runTest { + coEvery { homeRepository.getTheme() } returns null + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.initialDataLoading) + assertNotNull(viewModel.uiState.value) + } + + private fun getViewModel(): HomeViewModel { + return HomeViewModel(context, apiPrefs, homeRepository, localeUtils, aiAssistContextProvider) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/attachment/HorizonInboxAttachmentPickerViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/attachment/HorizonInboxAttachmentPickerViewModelTest.kt new file mode 100644 index 0000000000..8257c7a0ce --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/attachment/HorizonInboxAttachmentPickerViewModelTest.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.inbox.attachment + +import android.net.Uri +import com.instructure.canvasapi2.managers.FileUploadManager +import com.instructure.canvasapi2.models.Attachment +import com.instructure.canvasapi2.models.postmodels.FileSubmitObject +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.features.file.upload.FileUploadUtilsHelper +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class HorizonInboxAttachmentPickerViewModelTest { + private val fileUploadManager: FileUploadManager = mockk(relaxed = true) + private val fileUploadUtils: FileUploadUtilsHelper = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testUri: Uri = mockk(relaxed = true) + private val testFileSubmitObject = FileSubmitObject( + name = "test.pdf", + size = 1000L, + contentType = "application/pdf", + fullPath = "/path/to/test.pdf" + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + mockkStatic(Dispatchers::class) + every { Dispatchers.IO } returns testDispatcher + coEvery { fileUploadUtils.getFileSubmitObjectFromInputStream(any(), any(), any()) } returns testFileSubmitObject + every { fileUploadUtils.getFileNameWithDefault(any()) } returns "test.pdf" + every { fileUploadUtils.getFileMimeType(any()) } returns "application/pdf" + coEvery { fileUploadManager.uploadFile(any(), any(), any()) } returns DataResult.Success( + Attachment(id = 123L) + ) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel initializes with empty file list`() = runTest { + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.files.isEmpty()) + assertNotNull(viewModel.uiState.value.onFileSelected) + } + + @Test + fun `Test file selection adds file to list`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + assertEquals(1, viewModel.uiState.value.files.size) + assertEquals("test.pdf", viewModel.uiState.value.files.first().fileName) + assertEquals(1000L, viewModel.uiState.value.files.first().fileSize) + } + + @Test + fun `Test file upload starts with in-progress state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + val file = viewModel.uiState.value.files.first() + // Initially in progress, but will complete quickly due to UnconfinedTestDispatcher + assertTrue(file.state is HorizonInboxAttachmentState.InProgress || file.state is HorizonInboxAttachmentState.Success) + } + + @Test + fun `Test successful file upload sets success state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + // Wait for upload to complete + val file = viewModel.uiState.value.files.first() + assertTrue(file.state is HorizonInboxAttachmentState.Success) + assertEquals(123L, file.id) + } + + @Test + fun `Test failed file upload sets error state`() = runTest { + coEvery { fileUploadManager.uploadFile(any(), any(), any()) } returns DataResult.Fail() + + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + val file = viewModel.uiState.value.files.first() + assertTrue(file.state is HorizonInboxAttachmentState.Error) + } + + @Test + fun `Test successful upload adds remove action`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + val file = viewModel.uiState.value.files.first() + assertNotNull(file.onActionClicked) + } + + @Test + fun `Test remove action removes file from list`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + val file = viewModel.uiState.value.files.first() + file.onActionClicked?.invoke() + + assertTrue(viewModel.uiState.value.files.isEmpty()) + } + + @Test + fun `Test multiple file uploads`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + viewModel.uiState.value.onFileSelected(testUri) + + assertEquals(2, viewModel.uiState.value.files.size) + } + + @Test + fun `Test upload progress updates file state`() = runTest { + // This test verifies the upload manager's progress listener is called + coEvery { fileUploadManager.uploadFile(any(), any(), any()) } coAnswers { + val listener = secondArg() + listener.onProgressUpdated(50f, 1000L) + DataResult.Success(Attachment(id = 123L)) + } + + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + // File should eventually reach success state + val file = viewModel.uiState.value.files.first() + assertTrue(file.state is HorizonInboxAttachmentState.Success) + } + + @Test + fun `Test failed upload adds retry action`() = runTest { + coEvery { fileUploadManager.uploadFile(any(), any(), any()) } returns DataResult.Fail() + + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + val file = viewModel.uiState.value.files.first() + assertTrue(file.state is HorizonInboxAttachmentState.Error) + assertNotNull(file.onActionClicked) + } + + private fun getViewModel(): HorizonInboxAttachmentPickerViewModel { + return HorizonInboxAttachmentPickerViewModel(fileUploadManager, fileUploadUtils) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeRepositoryTest.kt new file mode 100644 index 0000000000..33c1ff5fb8 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeRepositoryTest.kt @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.inbox.compose + +import com.instructure.canvasapi2.CanvasRestAdapter +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.InboxApi +import com.instructure.canvasapi2.apis.RecipientAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Recipient +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkAll +import io.mockk.verify +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class HorizonInboxComposeRepositoryTest { + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val recipientApi: RecipientAPI.RecipientInterface = mockk(relaxed = true) + private val inboxApi: InboxApi.InboxInterface = mockk(relaxed = true) + + private lateinit var repository: HorizonInboxComposeRepository + + private val testCourses = listOf( + Course(id = 1L, name = "Course 1"), + Course(id = 2L, name = "Course 2") + ) + + private val testRecipients = listOf( + Recipient(stringId = "1", name = "Student 1"), + Recipient(stringId = "2", name = "Student 2"), + ) + + @Before + fun setup() { + repository = HorizonInboxComposeRepository(courseApi, recipientApi, inboxApi) + mockkObject(CanvasRestAdapter) + every { CanvasRestAdapter.clearCacheUrls(any()) } returns Unit + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getAllInboxCourses returns course list`() = runTest { + coEvery { courseApi.getFirstPageCoursesInbox(any()) } returns DataResult.Success(testCourses) + + val result = repository.getAllInboxCourses(forceNetwork = true) + + assertEquals(2, result.size) + assertEquals("Course 1", result.first().name) + coVerify { courseApi.getFirstPageCoursesInbox(any()) } + } + + @Test + fun `getAllInboxCourses with forceNetwork false`() = runTest { + coEvery { courseApi.getFirstPageCoursesInbox(any()) } returns DataResult.Success(testCourses) + + repository.getAllInboxCourses(forceNetwork = false) + + coVerify { courseApi.getFirstPageCoursesInbox(match { !it.isForceReadFromNetwork }) } + } + + @Test + fun `getAllInboxCourses with forceNetwork true`() = runTest { + coEvery { courseApi.getFirstPageCoursesInbox(any()) } returns DataResult.Success(testCourses) + + repository.getAllInboxCourses(forceNetwork = true) + + coVerify { courseApi.getFirstPageCoursesInbox(match { it.isForceReadFromNetwork }) } + } + + @Test + fun `getRecipients returns filtered list`() = runTest { + coEvery { recipientApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Success(testRecipients) + + val result = repository.getRecipients(courseId = 1L, searchQuery = "student") + + assertEquals(2, result.size) + assertTrue(result.all { it.recipientType == Recipient.Type.Person }) + coVerify { recipientApi.getFirstPageRecipientList("student", "1", any()) } + } + + @Test + fun `getRecipients filters out non-person recipients`() = runTest { + coEvery { recipientApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Success(testRecipients) + + val result = repository.getRecipients(courseId = 1L, searchQuery = null) + + assertEquals(2, result.size) + assertTrue(result.none { it.recipientType == Recipient.Type.Group }) + } + + @Test + fun `getRecipients with null search query`() = runTest { + coEvery { recipientApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Success(testRecipients) + + repository.getRecipients(courseId = 1L, searchQuery = null) + + coVerify { recipientApi.getFirstPageRecipientList(null, "1", any()) } + } + + @Test + fun `createConversation calls API with correct parameters`() = runTest { + coEvery { inboxApi.createConversation(any(), any(), any(), any(), any(), any(), any()) } returns mockk(relaxed = true) + + repository.createConversation( + recipientIds = listOf("1", "2"), + body = "Test message", + subject = "Test subject", + contextCode = "course_1", + attachmentIds = longArrayOf(100L, 200L), + isBulkMessage = false + ) + + coVerify { + inboxApi.createConversation( + recipients = listOf("1", "2"), + message = "Test message", + subject = "Test subject", + contextCode = "course_1", + isBulk = 0, + attachmentIds = longArrayOf(100L, 200L), + params = any() + ) + } + } + + @Test + fun `createConversation with bulk message flag`() = runTest { + coEvery { inboxApi.createConversation(any(), any(), any(), any(), any(), any(), any()) } returns mockk(relaxed = true) + + repository.createConversation( + recipientIds = listOf("1", "2"), + body = "Bulk message", + subject = "Bulk subject", + contextCode = "course_1", + attachmentIds = longArrayOf(), + isBulkMessage = true + ) + + coVerify { + inboxApi.createConversation( + recipients = any(), + message = any(), + subject = any(), + contextCode = any(), + isBulk = 1, + attachmentIds = any(), + params = any() + ) + } + } + + @Test + fun `invalidateConversationListCachedResponse clears cache`() { + repository.invalidateConversationListCachedResponse() + + verify { CanvasRestAdapter.clearCacheUrls("conversations") } + } + + @Test + fun `createConversation with empty attachments`() = runTest { + coEvery { inboxApi.createConversation(any(), any(), any(), any(), any(), any(), any()) } returns mockk(relaxed = true) + + repository.createConversation( + recipientIds = listOf("1"), + body = "Message", + subject = "Subject", + contextCode = "course_1", + attachmentIds = longArrayOf(), + isBulkMessage = false + ) + + coVerify { + inboxApi.createConversation( + recipients = any(), + message = any(), + subject = any(), + contextCode = any(), + isBulk = any(), + attachmentIds = longArrayOf(), + params = any() + ) + } + } + + @Test + fun `getRecipients with forceNetwork parameter`() = runTest { + coEvery { recipientApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Success(testRecipients) + + repository.getRecipients(courseId = 1L, searchQuery = "test", forceNetwork = true) + + coVerify { recipientApi.getFirstPageRecipientList(any(), any(), match { it.isForceReadFromNetwork }) } + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeViewModelTest.kt new file mode 100644 index 0000000000..4d3ea77e83 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeViewModelTest.kt @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.inbox.compose + +import android.content.Context +import androidx.compose.ui.text.input.TextFieldValue +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Recipient +import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachment +import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachmentState +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class HorizonInboxComposeViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: HorizonInboxComposeRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testCourses = listOf( + Course(id = 1L, name = "Course 1"), + Course(id = 2L, name = "Course 2") + ) + + private val testRecipients = listOf( + Recipient(stringId = "1", name = "Recipient 1"), + Recipient(stringId = "2", name = "Recipient 2") + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getAllInboxCourses(any()) } returns testCourses + coEvery { repository.getRecipients(any(), any()) } returns testRecipients + coEvery { repository.createConversation(any(), any(), any(), any(), any(), any()) } returns Unit + coEvery { repository.invalidateConversationListCachedResponse() } returns Unit + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel initializes with course list`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.coursePickerOptions.isEmpty()) + assertEquals(2, viewModel.uiState.value.coursePickerOptions.size) + coVerify { repository.getAllInboxCourses(forceNetwork = true) } + } + + @Test + fun `Test single course is auto-selected`() = runTest { + coEvery { repository.getAllInboxCourses(any()) } returns listOf(testCourses.first()) + + val viewModel = getViewModel() + + assertNotNull(viewModel.uiState.value.selectedCourse) + assertEquals(1L, viewModel.uiState.value.selectedCourse?.id) + } + + @Test + fun `Test multiple courses not auto-selected`() = runTest { + val viewModel = getViewModel() + + assertNull(viewModel.uiState.value.selectedCourse) + } + + @Test + fun `Test course selection updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onCourseSelected(testCourses.first()) + + assertEquals(1L, viewModel.uiState.value.selectedCourse?.id) + assertNull(viewModel.uiState.value.courseErrorMessage) + } + + @Test + fun `Test recipient search query change triggers fetch`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onCourseSelected(testCourses.first()) + + viewModel.uiState.value.onRecipientSearchQueryChanged(TextFieldValue("test")) + + assertEquals("test", viewModel.uiState.value.recipientSearchQuery.text) + } + + @Test + fun `Test recipient selection adds to list`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onRecipientSelected(testRecipients.first()) + + assertTrue(viewModel.uiState.value.selectedRecipients.contains(testRecipients.first())) + assertEquals("", viewModel.uiState.value.recipientSearchQuery.text) + assertNull(viewModel.uiState.value.recipientErrorMessage) + } + + @Test + fun `Test recipient removal updates list`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onRecipientSelected(testRecipients.first()) + + viewModel.onRecipientRemoved(testRecipients.first()) + + assertFalse(viewModel.uiState.value.selectedRecipients.contains(testRecipients.first())) + } + + @Test + fun `Test send individually checkbox updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onSendIndividuallyChanged(true) + + assertTrue(viewModel.uiState.value.isSendIndividually) + } + + @Test + fun `Test subject change updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onSubjectChanged(TextFieldValue("Test Subject")) + + assertEquals("Test Subject", viewModel.uiState.value.subject.text) + assertNull(viewModel.uiState.value.subjectErrorMessage) + } + + @Test + fun `Test body change updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onBodyChanged(TextFieldValue("Test Body")) + + assertEquals("Test Body", viewModel.uiState.value.body.text) + assertNull(viewModel.uiState.value.bodyErrorMessage) + } + + @Test + fun `Test send conversation validates required fields`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onSendConversation({}) + + assertNotNull(viewModel.uiState.value.courseErrorMessage) + assertNotNull(viewModel.uiState.value.recipientErrorMessage) + assertNotNull(viewModel.uiState.value.subjectErrorMessage) + assertNotNull(viewModel.uiState.value.bodyErrorMessage) + } + + @Test + fun `Test send conversation with valid data succeeds`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onCourseSelected(testCourses.first()) + viewModel.uiState.value.onRecipientSelected(testRecipients.first()) + viewModel.uiState.value.onSubjectChanged(TextFieldValue("Subject")) + viewModel.uiState.value.onBodyChanged(TextFieldValue("Body")) + + var finished = false + viewModel.uiState.value.onSendConversation { finished = true } + + coVerify { repository.createConversation(any(), any(), any(), any(), any(), any()) } + assertTrue(finished) + } + + @Test + fun `Test send conversation validates attachments are uploaded`() = runTest { + val viewModel = getViewModel() + val failedAttachment = HorizonInboxAttachment( + id = 1L, + fileName = "test.pdf", + fileSize = 1000L, + filePath = "/path", + state = HorizonInboxAttachmentState.Error + ) + viewModel.uiState.value.onCourseSelected(testCourses.first()) + viewModel.uiState.value.onRecipientSelected(testRecipients.first()) + viewModel.uiState.value.onSubjectChanged(TextFieldValue("Subject")) + viewModel.uiState.value.onBodyChanged(TextFieldValue("Body")) + viewModel.uiState.value.onAttachmentsChanged(listOf(failedAttachment)) + + viewModel.uiState.value.onSendConversation({}) + + assertNotNull(viewModel.uiState.value.attachmentsErrorMessage) + } + + @Test + fun `Test attachment picker visibility toggle`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onShowAttachmentPickerChanged(true) + + assertTrue(viewModel.uiState.value.showAttachmentPicker) + } + + @Test + fun `Test attachments change updates state`() = runTest { + val viewModel = getViewModel() + val attachment = HorizonInboxAttachment( + id = 1L, + fileName = "test.pdf", + fileSize = 1000L, + filePath = "/path", + state = HorizonInboxAttachmentState.Success + ) + + viewModel.uiState.value.onAttachmentsChanged(listOf(attachment)) + + assertEquals(1, viewModel.uiState.value.attachments.size) + assertEquals("test.pdf", viewModel.uiState.value.attachments.first().fileName) + } + + @Test + fun `Test exit confirmation dialog visibility toggle`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateShowExitConfirmationDialog(true) + + assertTrue(viewModel.uiState.value.showExitConfirmationDialog) + } + + @Test + fun `Test snackbar dismiss clears message`() = runTest { + coEvery { repository.getAllInboxCourses(any()) } throws Exception("Error") + + val viewModel = getViewModel() + assertNotNull(viewModel.uiState.value.snackbarMessage) + + viewModel.uiState.value.onDismissSnackbar() + + assertNull(viewModel.uiState.value.snackbarMessage) + } + + @Test + fun `Test fetch recipients with valid course`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onCourseSelected(testCourses.first()) + viewModel.uiState.value.onRecipientSearchQueryChanged(TextFieldValue("test query")) + + // Wait for debounce + delay(250) + + coVerify { repository.getRecipients(courseId = 1L, searchQuery = "test query", any()) } + } + + private fun getViewModel(): HorizonInboxComposeViewModel { + return HorizonInboxComposeViewModel(repository, context) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsRepositoryTest.kt new file mode 100644 index 0000000000..32293fbb79 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsRepositoryTest.kt @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.inbox.details + +import com.instructure.canvasapi2.CanvasRestAdapter +import com.instructure.canvasapi2.apis.AccountNotificationAPI +import com.instructure.canvasapi2.apis.AnnouncementAPI +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.InboxApi +import com.instructure.canvasapi2.models.AccountNotification +import com.instructure.canvasapi2.models.BasicUser +import com.instructure.canvasapi2.models.Conversation +import com.instructure.canvasapi2.models.DiscussionEntry +import com.instructure.canvasapi2.models.DiscussionParticipant +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkAll +import io.mockk.verify +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class HorizonInboxDetailsRepositoryTest { + private val inboxApi: InboxApi.InboxInterface = mockk(relaxed = true) + private val accountNotificationApi: AccountNotificationAPI.AccountNotificationInterface = mockk(relaxed = true) + private val announcementApi: AnnouncementAPI.AnnouncementInterface = mockk(relaxed = true) + private val discussionApi: DiscussionAPI.DiscussionInterface = mockk(relaxed = true) + + private lateinit var repository: HorizonInboxDetailsRepository + + private val testConversation = Conversation( + id = 1L, + subject = "Test Conversation", + participants = mutableListOf(BasicUser(id = 1L, name = "User 1")) + ) + + private val testAccountNotification = AccountNotification( + id = 100L, + subject = "Account Announcement", + message = "Test message" + ) + + private val testAnnouncement = DiscussionTopicHeader( + id = 200L, + title = "Course Announcement", + message = "Announcement message", + author = DiscussionParticipant(id = 1L, displayName = "Teacher") + ) + + private val testDiscussionTopic = DiscussionTopic( + views = mutableListOf( + DiscussionEntry(id = 1L, message = "Reply 1"), + DiscussionEntry(id = 2L, message = "Reply 2") + ) + ) + + @Before + fun setup() { + repository = HorizonInboxDetailsRepository(inboxApi, accountNotificationApi, announcementApi, discussionApi) + mockkObject(CanvasRestAdapter) + every { CanvasRestAdapter.clearCacheUrls(any()) } returns Unit + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getConversation returns conversation`() = runTest { + coEvery { inboxApi.getConversation(any(), any(), any()) } returns DataResult.Success(testConversation) + + val result = repository.getConversation(id = 1L, forceRefresh = false) + + assertEquals("Test Conversation", result.subject) + coVerify { inboxApi.getConversation(1L, true, any()) } + } + + @Test + fun `getConversation with forceRefresh true`() = runTest { + coEvery { inboxApi.getConversation(any(), any(), any()) } returns DataResult.Success(testConversation) + + repository.getConversation(id = 1L, forceRefresh = true) + + coVerify { inboxApi.getConversation(any(), any(), match { it.isForceReadFromNetwork }) } + } + + @Test + fun `getConversation with markAsRead false`() = runTest { + coEvery { inboxApi.getConversation(any(), any(), any()) } returns DataResult.Success(testConversation) + + repository.getConversation(id = 1L, markAsRead = false, forceRefresh = false) + + coVerify { inboxApi.getConversation(1L, false, any()) } + } + + @Test + fun `getAccountAnnouncement returns filtered announcement`() = runTest { + val announcements = listOf( + testAccountNotification, + AccountNotification(id = 101L, subject = "Other", message = "Other message") + ) + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns DataResult.Success(announcements) + + val result = repository.getAccountAnnouncement(id = 100L, forceRefresh = false) + + assertEquals("Account Announcement", result.subject) + coVerify { accountNotificationApi.getAccountNotifications(any(), true, true) } + } + + @Test + fun `getAnnouncement returns course announcement`() = runTest { + coEvery { announcementApi.getCourseAnnouncement(any(), any(), any()) } returns DataResult.Success(testAnnouncement) + + val result = repository.getAnnouncement(id = 200L, courseId = 1L, forceRefresh = false) + + assertEquals("Course Announcement", result.title) + coVerify { announcementApi.getCourseAnnouncement(1L, 200L, any()) } + } + + @Test + fun `getAnnouncementTopic returns discussion topic`() = runTest { + coEvery { discussionApi.getFullDiscussionTopic(any(), any(), any(), any(), any()) } returns DataResult.Success(testDiscussionTopic) + + val result = repository.getAnnouncementTopic(id = 200L, courseId = 1L, forceRefresh = false) + + assertEquals(2, result.views.size) + coVerify { discussionApi.getFullDiscussionTopic("courses", 1L, 200L, 1, any()) } + } + + @Test + fun `markAnnouncementAsRead marks topic and entries as read`() = runTest { + coEvery { discussionApi.markDiscussionTopicRead(any(), any(), any(), any()) } returns DataResult.Success(Unit) + coEvery { discussionApi.markDiscussionTopicEntryRead(any(), any(), any(), any(), any()) } returns DataResult.Success(Unit) + + val result = repository.markAnnouncementAsRead( + courseId = 1L, + announcementId = 200L, + entries = setOf(1L, 2L) + ) + + assertTrue(result is DataResult.Success) + coVerify { discussionApi.markDiscussionTopicRead("courses", 1L, 200L, any()) } + coVerify(exactly = 2) { discussionApi.markDiscussionTopicEntryRead("courses", 1L, 200L, any(), any()) } + } + + @Test + fun `markAnnouncementAsRead fails if topic marking fails`() = runTest { + coEvery { discussionApi.markDiscussionTopicRead(any(), any(), any(), any()) } returns DataResult.Fail() + + val result = repository.markAnnouncementAsRead( + courseId = 1L, + announcementId = 200L, + entries = setOf(1L, 2L) + ) + + assertTrue(result is DataResult.Fail) + } + + @Test + fun `markAnnouncementAsRead fails if not all entries marked`() = runTest { + coEvery { discussionApi.markDiscussionTopicRead(any(), any(), any(), any()) } returns DataResult.Success(Unit) + coEvery { discussionApi.markDiscussionTopicEntryRead(any(), any(), any(), 1L, any()) } returns DataResult.Success(Unit) + coEvery { discussionApi.markDiscussionTopicEntryRead(any(), any(), any(), 2L, any()) } returns DataResult.Fail() + + val result = repository.markAnnouncementAsRead( + courseId = 1L, + announcementId = 200L, + entries = setOf(1L, 2L) + ) + + assertTrue(result is DataResult.Fail) + } + + @Test + fun `addMessageToConversation adds message successfully`() = runTest { + coEvery { inboxApi.addMessage(any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testConversation) + + val result = repository.addMessageToConversation( + contextCode = "course_1", + conversationId = 1L, + recipientIds = listOf("1", "2"), + body = "Reply message", + includedMessageIds = listOf(10L, 20L), + attachmentIds = listOf(100L, 200L) + ) + + assertEquals("Test Conversation", result.subject) + coVerify { + inboxApi.addMessage( + conversationId = 1L, + recipientIds = listOf("1", "2"), + body = "Reply message", + includedMessageIds = longArrayOf(10L, 20L), + attachmentIds = longArrayOf(100L, 200L), + contextCode = "course_1", + params = any() + ) + } + } + + @Test + fun `addMessageToConversation with empty attachments`() = runTest { + coEvery { inboxApi.addMessage(any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testConversation) + + repository.addMessageToConversation( + contextCode = "course_1", + conversationId = 1L, + recipientIds = listOf("1"), + body = "Reply", + includedMessageIds = listOf(), + attachmentIds = listOf() + ) + + coVerify { + inboxApi.addMessage( + conversationId = any(), + recipientIds = any(), + body = any(), + includedMessageIds = longArrayOf(), + attachmentIds = longArrayOf(), + contextCode = any(), + params = any() + ) + } + } + + @Test + fun `invalidateConversationDetailsCachedResponse clears cache`() { + repository.invalidateConversationDetailsCachedResponse(conversationId = 1L) + + verify { CanvasRestAdapter.clearCacheUrls("conversations/1") } + } + + @Test + fun `markAnnouncementAsRead with empty entries succeeds`() = runTest { + coEvery { discussionApi.markDiscussionTopicRead(any(), any(), any(), any()) } returns DataResult.Success(Unit) + + val result = repository.markAnnouncementAsRead( + courseId = 1L, + announcementId = 200L, + entries = emptySet() + ) + + assertTrue(result is DataResult.Success) + coVerify(exactly = 0) { discussionApi.markDiscussionTopicEntryRead(any(), any(), any(), any(), any()) } + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsViewModelTest.kt new file mode 100644 index 0000000000..84b2773251 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsViewModelTest.kt @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.inbox.details + +import android.content.Context +import android.webkit.URLUtil +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.SavedStateHandle +import androidx.work.WorkManager +import com.instructure.canvasapi2.models.Attachment +import com.instructure.canvasapi2.models.BasicUser +import com.instructure.canvasapi2.models.Conversation +import com.instructure.canvasapi2.models.DiscussionParticipant +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Message +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.horizon.features.inbox.HorizonInboxItemType +import com.instructure.pandautils.room.appdatabase.daos.FileDownloadProgressDao +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class HorizonInboxDetailsViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: HorizonInboxDetailsRepository = mockk(relaxed = true) + private val workManager: WorkManager = mockk(relaxed = true) + private val fileDownloadProgressDao: FileDownloadProgressDao = mockk(relaxed = true) + private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testConversation = Conversation( + id = 1L, + subject = "Test Conversation", + messages = listOf( + Message( + id = 1L, + authorId = 1L, + body = "Test message", + createdAt = "2025-01-01T00:00:00Z", + attachments = arrayListOf() + ) + ), + participants = mutableListOf( + BasicUser(id = 1L, name = "Test User") + ) + ) + + private val testAnnouncement = DiscussionTopicHeader( + id = 1L, + title = "Test Announcement", + message = "Test message", + postedDate = Date(), + author = DiscussionParticipant(id = 1L, displayName = "Test Author") + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + mockkStatic(URLUtil::class) + every { URLUtil.isNetworkUrl(any()) } returns true + every { savedStateHandle.get(any()) } returns 1L + every { savedStateHandle.get("courseId") } returns "1" + every { savedStateHandle.get("type") } returns HorizonInboxItemType.Inbox.navigationValue + coEvery { repository.getConversation(any(), any(), any()) } returns testConversation + coEvery { repository.getAnnouncement(any(), any(), any()) } returns testAnnouncement + coEvery { repository.getAnnouncementTopic(any(), any(), any()) } returns mockk(relaxed = true) { + every { views } returns mutableListOf() + } + coEvery { repository.markAnnouncementAsRead(any(), any(), any()) } returns DataResult.Success(Unit) + coEvery { repository.addMessageToConversation(any(), any(), any(), any(), any(), any()) } returns testConversation + coEvery { repository.invalidateConversationDetailsCachedResponse(any()) } returns Unit + coEvery { fileDownloadProgressDao.findByWorkerIdFlow(any()) } returns flowOf(null) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel loads inbox conversation`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertEquals("Test Conversation", viewModel.uiState.value.title) + assertEquals(1, viewModel.uiState.value.items.size) + assertNotNull(viewModel.uiState.value.replyState) + coVerify { repository.getConversation(1L, true, any()) } + } + + @Test + fun `Test conversation message details are mapped correctly`() = runTest { + val viewModel = getViewModel() + + val item = viewModel.uiState.value.items.first() + assertEquals("Test User", item.author) + assertEquals("Test message", item.content) + assertFalse(item.isHtmlContent) + } + + @Test + fun `Test reply text change updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.replyState?.onReplyTextValueChange?.invoke(TextFieldValue("Reply text")) + + assertEquals("Reply text", viewModel.uiState.value.replyState?.replyTextValue?.text) + } + + @Test + fun `Test send reply adds message to conversation`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.replyState?.onReplyTextValueChange?.invoke(TextFieldValue("Reply text")) + + viewModel.uiState.value.replyState?.onSendReply?.invoke() + + coVerify { repository.addMessageToConversation(any(), any(), any(), "Reply text", any(), any()) } + coVerify { repository.invalidateConversationDetailsCachedResponse(1L) } + } + + @Test + fun `Test send reply clears input after success`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.replyState?.onReplyTextValueChange?.invoke(TextFieldValue("Reply text")) + + viewModel.uiState.value.replyState?.onSendReply?.invoke() + + assertEquals("", viewModel.uiState.value.replyState?.replyTextValue?.text) + assertFalse(viewModel.uiState.value.replyState?.isLoading == true) + } + + @Test + fun `Test send reply handles error`() = runTest { + coEvery { repository.addMessageToConversation(any(), any(), any(), any(), any(), any()) } throws Exception("Error") + + val viewModel = getViewModel() + viewModel.uiState.value.replyState?.onReplyTextValueChange?.invoke(TextFieldValue("Reply text")) + + viewModel.uiState.value.replyState?.onSendReply?.invoke() + + assertNotNull(viewModel.uiState.value.loadingState.snackbarMessage) + assertFalse(viewModel.uiState.value.replyState?.isLoading == true) + } + + @Test + fun `Test refresh reloads data`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.loadingState.onRefresh() + + coVerify(exactly = 2) { repository.getConversation(1L, any(), any()) } + } + + @Test + fun `Test refresh handles error`() = runTest { + val viewModel = getViewModel() + coEvery { repository.getConversation(any(), any(), any()) } throws Exception("Error") + + viewModel.uiState.value.loadingState.onRefresh() + + assertNotNull(viewModel.uiState.value.loadingState.snackbarMessage) + assertFalse(viewModel.uiState.value.loadingState.isRefreshing) + } + + @Test + fun `Test snackbar dismiss clears message`() = runTest { + coEvery { repository.getConversation(any(), any(), any()) } throws Exception("Error") + + val viewModel = getViewModel() + assertNotNull(viewModel.uiState.value.loadingState.snackbarMessage) + + viewModel.uiState.value.loadingState.onSnackbarDismiss() + + assertEquals(null, viewModel.uiState.value.loadingState.snackbarMessage) + } + + @Test + fun `Test attachment picker visibility toggle`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.replyState?.onShowAttachmentPickerChanged?.invoke(true) + + assertTrue(viewModel.uiState.value.replyState?.showAttachmentPicker == true) + } + + @Test + fun `Test attachments change updates reply state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.replyState?.onAttachmentsChanged?.invoke(emptyList()) + + assertEquals(0, viewModel.uiState.value.replyState?.attachments?.size) + } + + @Test + fun `Test exit confirmation dialog visibility toggle`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.replyState?.updateShowExitConfirmationDialog?.invoke(true) + + assertTrue(viewModel.uiState.value.replyState?.showExitConfirmationDialog == true) + } + + @Test + fun `Test invalid parameters set error state`() = runTest { + every { savedStateHandle.get(any()) } returns null + + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.loadingState.isError) + } + + @Test + fun `Test load error sets error state`() = runTest { + coEvery { repository.getConversation(any(), any(), any()) } throws Exception("Error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertNotNull(viewModel.uiState.value.loadingState.snackbarMessage) + } + + @Test + fun `Test conversation with attachments are mapped`() = runTest { + val conversationWithAttachment = testConversation.copy( + messages = listOf( + Message( + id = 1L, + authorId = 1L, + body = "Message with attachment", + createdAt = "2025-01-01T00:00:00Z", + attachments = arrayListOf( + Attachment( + id = 1L, + displayName = "test.pdf", + url = "http://example.com/test.pdf", + contentType = "application/pdf" + ) + ) + ) + ) + ) + coEvery { repository.getConversation(any(), any(), any()) } returns conversationWithAttachment + + val viewModel = getViewModel() + + val item = viewModel.uiState.value.items.first() + assertEquals(1, item.attachments.size) + assertEquals("test.pdf", item.attachments.first().name) + } + + @Test + fun `Test course announcement loads correctly`() = runTest { + every { savedStateHandle.get("type") } returns HorizonInboxItemType.CourseNotification.navigationValue + + val viewModel = getViewModel() + + assertEquals("Test Announcement", viewModel.uiState.value.title) + assertNotNull(viewModel.uiState.value.titleIcon) + coVerify { repository.getAnnouncement(1L, 1L, false) } + coVerify { repository.getAnnouncementTopic(1L, 1L, false) } + } + + private fun getViewModel(): HorizonInboxDetailsViewModel { + return HorizonInboxDetailsViewModel( + context, + repository, + workManager, + fileDownloadProgressDao, + savedStateHandle + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListRepositoryTest.kt new file mode 100644 index 0000000000..8dfba338ab --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListRepositoryTest.kt @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.inbox.list + +import com.instructure.canvasapi2.apis.AccountNotificationAPI +import com.instructure.canvasapi2.apis.AnnouncementAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.InboxApi +import com.instructure.canvasapi2.apis.RecipientAPI +import com.instructure.canvasapi2.models.AccountNotification +import com.instructure.canvasapi2.models.Conversation +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Recipient +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class HorizonInboxListRepositoryTest { + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val inboxApi: InboxApi.InboxInterface = mockk(relaxed = true) + private val recipientsApi: RecipientAPI.RecipientInterface = mockk(relaxed = true) + private val announcementsApi: AnnouncementAPI.AnnouncementInterface = mockk(relaxed = true) + private val accountNotificationApi: AccountNotificationAPI.AccountNotificationInterface = mockk(relaxed = true) + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + + private val userId = 1L + + @Before + fun setup() { + every { apiPrefs.user } returns User(id = userId, name = "Test User") + } + + @Test + fun `Test successful conversations retrieval`() = runTest { + val conversations = listOf( + Conversation(id = 1L, subject = "Conversation 1", lastMessage = "Message 1"), + Conversation(id = 2L, subject = "Conversation 2", lastMessage = "Message 2") + ) + coEvery { inboxApi.getConversations(any(), any()) } returns DataResult.Success(conversations) + + val result = getRepository().getConversations(forceNetwork = false) + + assertEquals(2, result.size) + assertEquals(conversations, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed conversations retrieval throws exception`() = runTest { + coEvery { inboxApi.getConversations(any(), any()) } returns DataResult.Fail() + + getRepository().getConversations(forceNetwork = false) + } + + @Test + fun `Test successful recipients retrieval filters by person type`() = runTest { + val recipients = listOf( + Recipient(stringId = "1", name = "Person 1"), + Recipient(stringId = "3", name = "Person 2") + ) + coEvery { recipientsApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Success(recipients) + + val result = getRepository().getRecipients("query", false) + + assertEquals(2, result.size) + assertTrue(result.all { it.recipientType == Recipient.Type.Person }) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed recipients retrieval throws exception`() = runTest { + coEvery { recipientsApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Fail() + + getRepository().getRecipients("query", false) + } + + @Test + fun `Test successful course announcements retrieval`() = runTest { + val course = Course(id = 1L, name = "Course 1",) + val announcement = DiscussionTopicHeader(id = 1L, title = "Announcement 1", contextCode = "course_1") + + coEvery { courseApi.getFirstPageCoursesInbox(any()) } returns DataResult.Success(listOf(course)) + coEvery { announcementsApi.getFirstPageAnnouncements(any(), startDate = any(), endDate = any(), params = any()) } returns + DataResult.Success(listOf(announcement)) + + val result = getRepository().getCourseAnnouncements(false) + + assertEquals(1, result.size) + assertEquals(course, result[0].first) + assertEquals(announcement, result[0].second) + } + + @Test + fun `Test course announcements with no courses returns empty list`() = runTest { + coEvery { courseApi.getFirstPageCoursesInbox(any()) } returns DataResult.Success(emptyList()) + + val result = getRepository().getCourseAnnouncements(false) + + assertTrue(result.isEmpty()) + } + + @Test + fun `Test successful account announcements retrieval`() = runTest { + val notifications = listOf( + AccountNotification(id = 1L, subject = "Notification 1"), + AccountNotification(id = 2L, subject = "Notification 2") + ) + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns + DataResult.Success(notifications) + + val result = getRepository().getAccountAnnouncements(false) + + assertEquals(2, result.size) + assertEquals(notifications, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed account announcements retrieval throws exception`() = runTest { + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns DataResult.Fail() + + getRepository().getAccountAnnouncements(false) + } + + @Test + fun `Test conversations scope filter`() = runTest { + val conversations = listOf(Conversation(id = 1L, subject = "Test")) + coEvery { inboxApi.getConversations(any(), any()) } returns DataResult.Success(conversations) + + getRepository().getConversations(InboxApi.Scope.SENT, false) + + coEvery { inboxApi.getConversations("sent", any()) } + } + + private fun getRepository(): HorizonInboxListRepository { + return HorizonInboxListRepository( + apiPrefs, + inboxApi, + recipientsApi, + announcementsApi, + accountNotificationApi, + courseApi + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListViewModelTest.kt new file mode 100644 index 0000000000..66fb4acf50 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListViewModelTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.inbox.list + +import android.content.Context +import com.instructure.canvasapi2.apis.InboxApi +import com.instructure.canvasapi2.models.Conversation +import com.instructure.canvasapi2.models.Recipient +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class HorizonInboxListViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: HorizonInboxListRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testConversations = listOf( + Conversation(id = 1L, subject = "Test 1", lastMessage = "Message 1"), + Conversation(id = 2L, subject = "Test 2", lastMessage = "Message 2") + ) + + private val testRecipients = listOf( + Recipient(stringId = "1", name = "Recipient 1"), + Recipient(stringId = "2", name = "Recipient 2") + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getConversations(any(), any()) } returns testConversations + coEvery { repository.getRecipients(any(), any()) } returns testRecipients + coEvery { repository.getCourseAnnouncements(any()) } returns emptyList() + coEvery { repository.getAccountAnnouncements(any()) } returns emptyList() + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads successfully`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + coVerify { repository.getConversations(InboxApi.Scope.INBOX, false) } + } + + @Test + fun `Test conversations are loaded`() = runTest { + val viewModel = getViewModel() + + coVerify { repository.getConversations(InboxApi.Scope.INBOX, false) } + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getConversations(any(), any()) } throws Exception("Network error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + } + + @Test + fun `Test scope filter change loads correct data`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateScopeFilter(HorizonInboxScope.Sent) + + coVerify { repository.getConversations(InboxApi.Scope.SENT, any()) } + } + + @Test + fun `Test unread scope filter`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateScopeFilter(HorizonInboxScope.Unread) + + coVerify { repository.getConversations(InboxApi.Scope.UNREAD, any()) } + } + + @Test + fun `Test announcements scope loads announcements`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateScopeFilter(HorizonInboxScope.Announcements) + + coVerify { repository.getCourseAnnouncements(any()) } + coVerify { repository.getAccountAnnouncements(any()) } + } + + @Test + fun `Test recipient selected adds to list`() = runTest { + val viewModel = getViewModel() + + val recipient = Recipient(stringId = "3", name = "New Recipient") + viewModel.uiState.value.onRecipientSelected(recipient) + + assertTrue(viewModel.uiState.value.selectedRecipients.contains(recipient)) + } + + @Test + fun `Test recipient removed from list`() = runTest { + val viewModel = getViewModel() + + val recipient = Recipient(stringId = "3", name = "Recipient") + viewModel.uiState.value.onRecipientSelected(recipient) + viewModel.uiState.value.onRecipientRemoved(recipient) + + assertFalse(viewModel.uiState.value.selectedRecipients.contains(recipient)) + } + + @Test + fun `Test refresh reloads data`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.loadingState.onRefresh() + + coVerify(atLeast = 2) { repository.getConversations(any(), any()) } + } + + private fun getViewModel(): HorizonInboxListViewModel { + return HorizonInboxListViewModel(context, repository) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnRepositoryTest.kt new file mode 100644 index 0000000000..a2174615fe --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnRepositoryTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.learn + +import com.instructure.canvasapi2.managers.CourseWithModuleItemDurations +import com.instructure.canvasapi2.managers.CourseWithProgress +import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.JourneyApiManager +import com.instructure.canvasapi2.managers.graphql.Program +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.journey.type.ProgramVariantType +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class LearnRepositoryTest { + private val horizonGetCoursesManager: HorizonGetCoursesManager = mockk(relaxed = true) + private val journeyApiManager: JourneyApiManager = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + + private val userId = 1L + + @Before + fun setup() { + every { apiPrefs.user } returns User(id = userId, name = "Test User") + } + + @Test + fun `Test successful courses with progress retrieval`() = runTest { + val courses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Course 1", courseSyllabus = "", progress = 50.0), + CourseWithProgress(courseId = 2L, courseName = "Course 2", courseSyllabus = "", progress = 75.0) + ) + coEvery { horizonGetCoursesManager.getCoursesWithProgress(userId, false) } returns DataResult.Success(courses) + + val result = getRepository().getCoursesWithProgress(false) + + assertEquals(2, result.size) + assertEquals(courses, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed courses retrieval throws exception`() = runTest { + coEvery { horizonGetCoursesManager.getCoursesWithProgress(userId, false) } returns DataResult.Fail() + + getRepository().getCoursesWithProgress(false) + } + + @Test + fun `Test successful programs retrieval`() = runTest { + val programs = listOf( + Program( + id = "1", + name = "Program 1", + description = "Program 1 Description", + sortedRequirements = emptyList(), + startDate = null, + endDate = null, + variant = ProgramVariantType.LINEAR, + ), + Program( + id = "2", + name = "Program 2", + description = "Program 2 Description", + sortedRequirements = emptyList(), + startDate = null, + endDate = null, + variant = ProgramVariantType.NON_LINEAR, + ) + ) + coEvery { journeyApiManager.getPrograms(false) } returns programs + + val result = getRepository().getPrograms(false) + + assertEquals(2, result.size) + assertEquals(programs, result) + } + + @Test + fun `Test successful get courses by id`() = runTest { + val courseIds = listOf(1L, 2L) + val course1 = CourseWithModuleItemDurations(courseId = 1L, courseName = "Course 1") + val course2 = CourseWithModuleItemDurations(courseId = 2L, courseName = "Course 2") + + coEvery { horizonGetCoursesManager.getProgramCourses(1L, false) } returns DataResult.Success(course1) + coEvery { horizonGetCoursesManager.getProgramCourses(2L, false) } returns DataResult.Success(course2) + + val result = getRepository().getCoursesById(courseIds, false) + + assertEquals(2, result.size) + assertEquals(course1, result[0]) + assertEquals(course2, result[1]) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed get courses by id throws exception`() = runTest { + val courseIds = listOf(1L) + coEvery { horizonGetCoursesManager.getProgramCourses(1L, false) } returns DataResult.Fail() + + getRepository().getCoursesById(courseIds, false) + } + + private fun getRepository(): LearnRepository { + return LearnRepository(horizonGetCoursesManager, journeyApiManager, apiPrefs) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnViewModelTest.kt new file mode 100644 index 0000000000..82bc5a3e5f --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnViewModelTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.learn + +import android.content.Context +import androidx.lifecycle.SavedStateHandle +import com.instructure.canvasapi2.managers.CourseWithModuleItemDurations +import com.instructure.canvasapi2.managers.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.Program +import com.instructure.canvasapi2.managers.graphql.ProgramRequirement +import com.instructure.journey.type.ProgramProgressCourseEnrollmentStatus +import com.instructure.journey.type.ProgramVariantType +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class LearnViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: LearnRepository = mockk(relaxed = true) + private val savedStateHandle: SavedStateHandle = SavedStateHandle() + private val testDispatcher = UnconfinedTestDispatcher() + + private val testCourses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Course 1", courseSyllabus = "", progress = 50.0), + CourseWithProgress(courseId = 2L, courseName = "Course 2", courseSyllabus = "", progress = 75.0), + CourseWithProgress(courseId = 3L, courseName = "Course 3", courseSyllabus = "", progress = 25.0) + ) + + private val testProgram = Program( + id = "prog1", + name = "Program 1", + description = "Program 1 description", + sortedRequirements = listOf( + ProgramRequirement( + id = "req1", + progressId = "prog1", + courseId = 1L, + required = true, + progress = 2.0, + enrollmentStatus = ProgramProgressCourseEnrollmentStatus.ENROLLED + ) + ), + startDate = null, + endDate = null, + variant = ProgramVariantType.LINEAR, + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getPrograms(any()) } returns listOf(testProgram) + coEvery { repository.getCoursesWithProgress(any()) } returns testCourses + coEvery { repository.getCoursesById(any(), any()) } returns listOf( + CourseWithModuleItemDurations(courseId = 1L, courseName = "Course 1") + ) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads successfully`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.state.value.screenState.isLoading) + assertTrue(viewModel.state.value.learningItems.isNotEmpty()) + } + + @Test + fun `Test standalone courses are separated from program courses`() = runTest { + val viewModel = getViewModel() + + val learningItems = viewModel.state.value.learningItems + + assertTrue(learningItems.any { item -> + item is LearningItem.CourseItem && item.courseWithProgress.courseId == 2L + }) + assertTrue(learningItems.any { item -> + item is LearningItem.CourseItem && item.courseWithProgress.courseId == 3L + }) + } + + @Test + fun `Test program courses are grouped`() = runTest { + val viewModel = getViewModel() + + val learningItems = viewModel.state.value.learningItems + + assertTrue(learningItems.any { item -> + item is LearningItem.ProgramGroupItem && item.programName == "Program 1" + }) + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getPrograms(any()) } throws Exception("Network error") + + val viewModel = getViewModel() + + assertFalse(viewModel.state.value.screenState.isLoading) + assertTrue(viewModel.state.value.screenState.isError) + } + + @Test + fun `Test empty courses list`() = runTest { + coEvery { repository.getCoursesWithProgress(any()) } returns emptyList() + coEvery { repository.getPrograms(any()) } returns emptyList() + + val viewModel = getViewModel() + + assertFalse(viewModel.state.value.screenState.isLoading) + assertTrue(viewModel.state.value.learningItems.isEmpty()) + } + + @Test + fun `Test learningItemId from saved state`() = runTest { + val savedStateWithId = SavedStateHandle(mapOf("learningItemId" to "testId")) + + val viewModel = LearnViewModel(context, repository, savedStateWithId) + + assertFalse(viewModel.state.value.screenState.isLoading) + } + + private fun getViewModel(): LearnViewModel { + return LearnViewModel(context, repository, savedStateHandle) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceRepositoryTest.kt new file mode 100644 index 0000000000..e5747c33c9 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceRepositoryTest.kt @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.moduleitemsequence + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.managers.HorizonGetCommentsManager +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleItemSequence +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Before +import org.junit.Test + +class ModuleItemSequenceRepositoryTest { + private val moduleApi: ModuleAPI.ModuleInterface = mockk(relaxed = true) + private val assignmentApi: AssignmentAPI.AssignmentInterface = mockk(relaxed = true) + private val horizonGetCommentsManager: HorizonGetCommentsManager = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + + private val userId = 1L + private val courseId = 1L + + @Before + fun setup() { + every { apiPrefs.user } returns User(id = userId, name = "Test User") + } + + @Test + fun `Test successful module item sequence retrieval`() = runTest { + val sequence = ModuleItemSequence() + coEvery { moduleApi.getModuleItemSequence(any(), any(), any(), any(), any()) } returns + DataResult.Success(sequence) + + val result = getRepository().getModuleItemSequence(courseId, "Assignment", "123") + + assertEquals(sequence, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed module item sequence retrieval throws exception`() = runTest { + coEvery { moduleApi.getModuleItemSequence(any(), any(), any(), any(), any()) } returns DataResult.Fail() + + getRepository().getModuleItemSequence(courseId, "Assignment", "123") + } + + @Test + fun `Test successful modules with items retrieval`() = runTest { + val module = ModuleObject(id = 1L, name = "Module 1", itemCount = 2, items = listOf( + ModuleItem(id = 1L, title = "Item 1"), + ModuleItem(id = 2L, title = "Item 2") + )) + coEvery { moduleApi.getFirstPageModulesWithItems(any(), any(), any(), any()) } returns + DataResult.Success(listOf(module)) + + val result = getRepository().getModulesWithItems(courseId, false) + + assertEquals(1, result.size) + assertEquals(module, result[0]) + } + + @Test + fun `Test modules with incomplete items fetches all items`() = runTest { + val incompleteModule = ModuleObject(id = 1L, name = "Module 1", itemCount = 3, items = listOf( + ModuleItem(id = 1L, title = "Item 1") + )) + val allItems = listOf( + ModuleItem(id = 1L, title = "Item 1"), + ModuleItem(id = 2L, title = "Item 2"), + ModuleItem(id = 3L, title = "Item 3") + ) + + coEvery { moduleApi.getFirstPageModulesWithItems(any(), any(), any(), any()) } returns + DataResult.Success(listOf(incompleteModule)) + coEvery { moduleApi.getFirstPageModuleItems(any(), any(), any(), any(), any()) } returns + DataResult.Success(allItems) + + val result = getRepository().getModulesWithItems(courseId, false) + + assertEquals(1, result.size) + assertEquals(3, result[0].items.size) + } + + @Test + fun `Test successful module item retrieval`() = runTest { + val moduleItem = ModuleItem(id = 1L, title = "Item 1", moduleId = 1L) + coEvery { moduleApi.getModuleItem(any(), any(), any(), any(), any()) } returns + DataResult.Success(moduleItem) + + val result = getRepository().getModuleItem(courseId, 1L, 1L) + + assertEquals(moduleItem, result) + } + + @Test + fun `Test mark as done success`() = runTest { + val moduleItem = ModuleItem(id = 1L, moduleId = 1L) + coEvery { moduleApi.markModuleItemAsDone(any(), any(), any(), any(), any()) } returns + DataResult.Success("".toResponseBody()) + + val result = getRepository().markAsDone(courseId, moduleItem) + + assertTrue(result is DataResult.Success) + } + + @Test + fun `Test mark as not done success`() = runTest { + val moduleItem = ModuleItem(id = 1L, moduleId = 1L) + coEvery { moduleApi.markModuleItemAsNotDone(any(), any(), any(), any(), any()) } returns + DataResult.Success("".toResponseBody()) + + val result = getRepository().markAsNotDone(courseId, moduleItem) + + assertTrue(result is DataResult.Success) + } + + @Test + fun `Test mark as read`() = runTest { + coEvery { moduleApi.markModuleItemRead(any(), any(), any(), any(), any()) } returns + DataResult.Success("".toResponseBody()) + + getRepository().markAsRead(courseId, 1L, 1L) + + coVerify { moduleApi.markModuleItemRead(any(), courseId, 1L, 1L, any()) } + } + + @Test + fun `Test successful assignment retrieval`() = runTest { + val assignment = Assignment(id = 1L, name = "Assignment 1") + coEvery { assignmentApi.getAssignmentWithHistory(any(), any(), any()) } returns + DataResult.Success(assignment) + + val result = getRepository().getAssignment(1L, courseId, false) + + assertEquals(assignment, result) + } + + @Test + fun `Test has unread comments returns true when count greater than zero`() = runTest { + coEvery { horizonGetCommentsManager.getUnreadCommentsCount(any(), any(), any()) } returns 5 + + val result = getRepository().hasUnreadComments(1L, false) + + assertTrue(result) + } + + @Test + fun `Test has unread comments returns false when count is zero`() = runTest { + coEvery { horizonGetCommentsManager.getUnreadCommentsCount(any(), any(), any()) } returns 0 + + val result = getRepository().hasUnreadComments(1L, false) + + assertFalse(result) + } + + @Test + fun `Test has unread comments returns false when assignment id is null`() = runTest { + val result = getRepository().hasUnreadComments(null, false) + + assertFalse(result) + } + + private fun getRepository(): ModuleItemSequenceRepository { + return ModuleItemSequenceRepository(moduleApi, assignmentApi, horizonGetCommentsManager, apiPrefs) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModelTest.kt new file mode 100644 index 0000000000..18366d9a38 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModelTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.moduleitemsequence + +import android.content.Context +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.toRoute +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleItemSequence +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider +import com.instructure.horizon.horizonui.organisms.cards.ModuleItemCardStateMapper +import com.instructure.horizon.navigation.MainNavigationRoute +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class ModuleItemSequenceViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: ModuleItemSequenceRepository = mockk(relaxed = true) + private val moduleItemCardStateMapper: ModuleItemCardStateMapper = mockk(relaxed = true) + private val aiAssistContextProvider: AiAssistContextProvider = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) + + private val courseId = 1L + private val moduleItemId = 100L + + private val testModuleItem = ModuleItem( + id = moduleItemId, + title = "Test Item", + moduleId = 1L, + type = "Assignment" + ) + + private val testModule = ModuleObject( + id = 1L, + name = "Test Module", + items = listOf(testModuleItem) + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + mockkStatic("androidx.navigation.SavedStateHandleKt") + every { savedStateHandle.toRoute() } returns MainNavigationRoute.ModuleItemSequence( + courseId = courseId, + moduleItemId = moduleItemId, + moduleItemAssetType = null, + moduleItemAssetId = null + ) + coEvery { repository.getModuleItemSequence(any(), any(), any()) } returns ModuleItemSequence() + coEvery { repository.getModulesWithItems(any(), any()) } returns listOf(testModule) + coEvery { repository.getModuleItem(any(), any(), any()) } returns testModuleItem + coEvery { repository.getAssignment(any(), any(), any()) } returns Assignment(id = 1L) + coEvery { repository.hasUnreadComments(any(), any()) } returns false + coEvery { moduleItemCardStateMapper.mapModuleItemToCardState(any(), any()) } returns mockk(relaxed = true) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads with moduleItemId`() = runTest { + val viewModel = getViewModel(savedStateHandle) + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + coVerify { repository.getModulesWithItems(courseId, any()) } + } + + @Test + fun `Test module items are loaded`() = runTest { + val viewModel = getViewModel(savedStateHandle) + + assertNotNull(viewModel.uiState.value) + coVerify { repository.getModulesWithItems(courseId, true) } + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getModulesWithItems(any(), any()) } throws Exception("Network error") + + val viewModel = getViewModel(savedStateHandle) + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + } + + @Test + fun `Test assignment is fetched for module item`() = runTest { + coEvery { repository.getAssignment(any(), any(), any()) } returns Assignment(id = 123L, name = "Test Assignment") + + val viewModel = getViewModel(savedStateHandle) + + coVerify { repository.getAssignment(any(), courseId, any()) } + } + + @Test + fun `Test unread comments check is performed`() = runTest { + val assignment = Assignment(id = 123L) + coEvery { repository.getAssignment(any(), any(), any()) } returns assignment + + val viewModel = getViewModel(savedStateHandle) + + coVerify { repository.hasUnreadComments(123L, any()) } + } + + private fun getViewModel(savedStateHandle: SavedStateHandle): ModuleItemSequenceViewModel { + return ModuleItemSequenceViewModel( + context, + repository, + moduleItemCardStateMapper, + aiAssistContextProvider, + savedStateHandle + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentRepositoryTest.kt new file mode 100644 index 0000000000..5ba75ef571 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentRepositoryTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.moduleitemsequence.content.assessment + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class AssessmentRepositoryTest { + private val assignmentApi: AssignmentAPI.AssignmentInterface = mockk(relaxed = true) + private val oAuthInterface: OAuthAPI.OAuthInterface = mockk(relaxed = true) + private val launchDefinitionsApi: LaunchDefinitionsAPI.LaunchDefinitionsInterface = mockk(relaxed = true) + + private lateinit var repository: AssessmentRepository + + private val testAssignment = Assignment( + id = 1L, + name = "Test Quiz", + courseId = 100L, + url = "https://example.com/quiz/1" + ) + + private val testLTITool = LTITool( + url = "https://lti.example.com/tool", + id = 1 + ) + + @Before + fun setup() { + repository = AssessmentRepository(assignmentApi, oAuthInterface, launchDefinitionsApi) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getAssignment returns assignment successfully`() = runTest { + coEvery { assignmentApi.getAssignmentWithHistory(any(), any(), any()) } returns DataResult.Success(testAssignment) + + val result = repository.getAssignment(assignmentId = 1L, courseId = 100L, forceNetwork = false) + + assertEquals("Test Quiz", result.name) + assertEquals(1L, result.id) + coVerify { assignmentApi.getAssignmentWithHistory(100L, 1L, any()) } + } + + @Test + fun `getAssignment with forceNetwork true`() = runTest { + coEvery { assignmentApi.getAssignmentWithHistory(any(), any(), any()) } returns DataResult.Success(testAssignment) + + repository.getAssignment(assignmentId = 1L, courseId = 100L, forceNetwork = true) + + coVerify { assignmentApi.getAssignmentWithHistory(any(), any(), match { it.isForceReadFromNetwork }) } + } + + @Test + fun `getAssignment with forceNetwork false`() = runTest { + coEvery { assignmentApi.getAssignmentWithHistory(any(), any(), any()) } returns DataResult.Success(testAssignment) + + repository.getAssignment(assignmentId = 1L, courseId = 100L, forceNetwork = false) + + coVerify { assignmentApi.getAssignmentWithHistory(any(), any(), match { !it.isForceReadFromNetwork }) } + } + + @Test + fun `authenticateUrl returns authenticated URL for LTI tool`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://authenticated.lti.url") + coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(testLTITool) + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + val result = repository.authenticateUrl("https://example.com/quiz") + + assertEquals("https://authenticated.lti.url", result) + coVerify { launchDefinitionsApi.getLtiFromAuthenticationUrl("https://example.com/quiz", any()) } + coVerify { oAuthInterface.getAuthenticatedSession("https://lti.example.com/tool", any()) } + } + + @Test + fun `authenticateUrl returns original URL when LTI tool URL is null`() = runTest { + val ltiToolWithoutUrl = LTITool(url = null, id = 1) + coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(ltiToolWithoutUrl) + + val result = repository.authenticateUrl("https://example.com/quiz") + + assertEquals("https://example.com/quiz", result) + } + + @Test + fun `authenticateUrl returns original URL when session URL is null`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://example.com/quiz/authenticated") + coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(testLTITool) + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + val result = repository.authenticateUrl("https://example.com/quiz") + + assertEquals("https://example.com/quiz/authenticated", result) + } + + @Test + fun `authenticateUrl returns original URL when authentication fails`() = runTest { + coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(testLTITool) + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Fail() + + val result = repository.authenticateUrl("https://example.com/quiz") + + assertEquals("https://example.com/quiz", result) + } + + @Test + fun `authenticateUrl always uses forceNetwork`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://authenticated.url") + coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(testLTITool) + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + repository.authenticateUrl("https://example.com") + + coVerify { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), match { it.isForceReadFromNetwork }) } + coVerify { oAuthInterface.getAuthenticatedSession(any(), match { it.isForceReadFromNetwork }) } + } + + @Test + fun `getAssignment with different course and assignment IDs`() = runTest { + coEvery { assignmentApi.getAssignmentWithHistory(any(), any(), any()) } returns DataResult.Success(testAssignment) + + repository.getAssignment(assignmentId = 99L, courseId = 200L, forceNetwork = false) + + coVerify { assignmentApi.getAssignmentWithHistory(200L, 99L, any()) } + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentViewModelTest.kt new file mode 100644 index 0000000000..2ededc5f96 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentViewModelTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.moduleitemsequence.content.assessment + +import androidx.lifecycle.SavedStateHandle +import com.instructure.canvasapi2.models.Assignment +import com.instructure.horizon.features.moduleitemsequence.ModuleItemContent +import com.instructure.pandautils.utils.Const +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AssessmentViewModelTest { + private val repository: AssessmentRepository = mockk(relaxed = true) + private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val assignmentId = 1L + private val courseId = 100L + private val testAssignment = Assignment( + id = assignmentId, + name = "Test Quiz", + url = "https://example.com/quiz/1" + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + every { savedStateHandle.get(ModuleItemContent.Assignment.ASSIGNMENT_ID) } returns assignmentId + every { savedStateHandle.get(Const.COURSE_ID) } returns courseId + coEvery { repository.getAssignment(any(), any(), any()) } returns testAssignment + coEvery { repository.authenticateUrl(any()) } returns "https://authenticated.url" + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel loads assignment data`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertEquals("Test Quiz", viewModel.uiState.value.assessmentName) + coVerify { repository.getAssignment(assignmentId, courseId, false) } + } + + @Test + fun `Test start quiz clicked shows dialog and loads URL`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onStartQuizClicked() + + assertTrue(viewModel.uiState.value.showAssessmentDialog) + assertEquals("https://authenticated.url", viewModel.uiState.value.urlToLoad) + viewModel.uiState.value.onAssessmentLoaded() + assertFalse(viewModel.uiState.value.assessmentLoading) + coVerify { repository.authenticateUrl("https://example.com/quiz/1") } + } + + @Test + fun `Test start quiz with authentication error`() = runTest { + coEvery { repository.authenticateUrl(any()) } throws Exception("Auth error") + + val viewModel = getViewModel() + + viewModel.uiState.value.onStartQuizClicked() + + assertTrue(viewModel.uiState.value.showAssessmentDialog) + assertFalse(viewModel.uiState.value.assessmentLoading) + } + + @Test + fun `Test assessment closed clears URL and dialog`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onStartQuizClicked() + + viewModel.uiState.value.onAssessmentClosed() + + assertNull(viewModel.uiState.value.urlToLoad) + assertFalse(viewModel.uiState.value.showAssessmentDialog) + } + + @Test + fun `Test assessment loaded clears loading state`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onStartQuizClicked() + + viewModel.uiState.value.onAssessmentLoaded() + + assertFalse(viewModel.uiState.value.assessmentLoading) + } + + @Test + fun `Test assessment completion starts loading`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onAssessmentCompletion() + + assertTrue(viewModel.uiState.value.assessmentCompletionLoading) + } + + @Test + fun `Test assessment completion finishes after delay`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onAssessmentCompletion() + assertTrue(viewModel.uiState.value.assessmentCompletionLoading) + + advanceTimeBy(15100) + + assertFalse(viewModel.uiState.value.assessmentCompletionLoading) + } + + @Test + fun `Test load error sets error state`() = runTest { + coEvery { repository.getAssignment(any(), any(), any()) } throws Exception("Error") + + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.loadingState.isError) + } + + @Test + fun `Test start quiz with null assessment URL`() = runTest { + coEvery { repository.getAssignment(any(), any(), any()) } returns testAssignment.copy(url = null) + + val viewModel = getViewModel() + + viewModel.uiState.value.onStartQuizClicked() + + assertTrue(viewModel.uiState.value.showAssessmentDialog) + assertNull(viewModel.uiState.value.urlToLoad) + assertFalse(viewModel.uiState.value.assessmentLoading) + } + + @Test + fun `Test UI state contains all callbacks`() = runTest { + val viewModel = getViewModel() + + assertNotNull(viewModel.uiState.value.onAssessmentClosed) + assertNotNull(viewModel.uiState.value.onStartQuizClicked) + assertNotNull(viewModel.uiState.value.onAssessmentCompletion) + assertNotNull(viewModel.uiState.value.onAssessmentLoaded) + } + + private fun getViewModel(): AssessmentViewModel { + return AssessmentViewModel(repository, savedStateHandle) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsRepositoryTest.kt new file mode 100644 index 0000000000..3cf9826bed --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsRepositoryTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.moduleitemsequence.content.assignment + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.managers.HorizonGetCommentsManager +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class AssignmentDetailsRepositoryTest { + private val assignmentApi: AssignmentAPI.AssignmentInterface = mockk(relaxed = true) + private val oAuthInterface: OAuthAPI.OAuthInterface = mockk(relaxed = true) + private val horizonGetCommentsManager: HorizonGetCommentsManager = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + + private val userId = 1L + private val courseId = 1L + private val assignmentId = 1L + + @Before + fun setup() { + every { apiPrefs.user } returns User(id = userId, name = "Test User") + } + + @Test + fun `Test successful assignment retrieval`() = runTest { + val assignment = Assignment(id = assignmentId, name = "Test Assignment", pointsPossible = 100.0) + coEvery { assignmentApi.getAssignmentWithHistory(courseId, assignmentId, any()) } returns + DataResult.Success(assignment) + + val result = getRepository().getAssignment(assignmentId, courseId, false) + + assertEquals(assignment, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed assignment retrieval throws exception`() = runTest { + coEvery { assignmentApi.getAssignmentWithHistory(courseId, assignmentId, any()) } returns + DataResult.Fail() + + getRepository().getAssignment(assignmentId, courseId, false) + } + + @Test + fun `Test successful URL authentication`() = runTest { + val originalUrl = "https://example.com/file" + val authenticatedUrl = "https://example.com/file?session=xyz" + val session = AuthenticatedSession(sessionUrl = authenticatedUrl) + + coEvery { oAuthInterface.getAuthenticatedSession(originalUrl, any()) } returns + DataResult.Success(session) + + val result = getRepository().authenticateUrl(originalUrl) + + assertEquals(authenticatedUrl, result) + } + + @Test + fun `Test URL authentication fallback on failure`() = runTest { + val originalUrl = "https://example.com/file" + coEvery { oAuthInterface.getAuthenticatedSession(originalUrl, any()) } returns DataResult.Fail() + + val result = getRepository().authenticateUrl(originalUrl) + + assertEquals(originalUrl, result) + } + + @Test + fun `Test URL authentication fallback on null session`() = runTest { + val originalUrl = "https://example.com/file" + coEvery { oAuthInterface.getAuthenticatedSession(originalUrl, any()) } returns DataResult.Fail() + + val result = getRepository().authenticateUrl(originalUrl) + + assertEquals(originalUrl, result) + } + + @Test + fun `Test has unread comments returns true when count greater than zero`() = runTest { + coEvery { horizonGetCommentsManager.getUnreadCommentsCount(assignmentId, userId, false) } returns 3 + + val result = getRepository().hasUnreadComments(assignmentId, false) + + assertTrue(result) + } + + @Test + fun `Test has unread comments returns false when count is zero`() = runTest { + coEvery { horizonGetCommentsManager.getUnreadCommentsCount(assignmentId, userId, false) } returns 0 + + val result = getRepository().hasUnreadComments(assignmentId, false) + + assertFalse(result) + } + + @Test + fun `Test force network parameter is passed correctly`() = runTest { + val assignment = Assignment(id = assignmentId, name = "Test Assignment") + coEvery { assignmentApi.getAssignmentWithHistory(courseId, assignmentId, any()) } returns + DataResult.Success(assignment) + + getRepository().getAssignment(assignmentId, courseId, true) + + coEvery { assignmentApi.getAssignmentWithHistory(courseId, assignmentId, match { it.isForceReadFromNetwork }) } + } + + private fun getRepository(): AssignmentDetailsRepository { + return AssignmentDetailsRepository(assignmentApi, oAuthInterface, horizonGetCommentsManager, apiPrefs) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsViewModelTest.kt new file mode 100644 index 0000000000..5863270c81 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsViewModelTest.kt @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.moduleitemsequence.content.assignment + +import android.content.Context +import androidx.lifecycle.SavedStateHandle +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Submission +import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider +import com.instructure.horizon.features.moduleitemsequence.ModuleItemContent +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.HtmlContentFormatter +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AssignmentDetailsViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: AssignmentDetailsRepository = mockk(relaxed = true) + private val htmlContentFormatter: HtmlContentFormatter = mockk(relaxed = true) + private val aiAssistContextProvider: AiAssistContextProvider = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val courseId = 1L + private val assignmentId = 100L + + private val testAssignment = Assignment( + id = assignmentId, + name = "Test Assignment", + pointsPossible = 100.0, + description = "Test description", + allowedAttempts = 3L + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getAssignment(any(), any(), any()) } returns testAssignment + coEvery { repository.hasUnreadComments(any(), any()) } returns false + coEvery { repository.authenticateUrl(any()) } returns "https://authenticated.url" + coEvery { htmlContentFormatter.formatHtmlWithIframes(any()) } returns "Formatted content" + coEvery { aiAssistContextProvider.aiAssistContext } returns mockk(relaxed = true) + coEvery { aiAssistContextProvider.aiAssistContext = any() } returns Unit + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads with assignmentId and courseId`() = runTest { + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + coVerify { repository.getAssignment(assignmentId, courseId, false) } + } + + @Test + fun `Test assignment is loaded successfully`() = runTest { + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertNotNull(viewModel.assignmentFlow.value) + assertEquals(testAssignment, viewModel.assignmentFlow.value) + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getAssignment(any(), any(), any()) } throws Exception("Network error") + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + } + + @Test + fun `Test unread comments check is performed`() = runTest { + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + coVerify { repository.hasUnreadComments(assignmentId, false) } + } + + @Test + fun `Test unread comments flag is set correctly`() = runTest { + coEvery { repository.hasUnreadComments(any(), any()) } returns true + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertTrue(viewModel.uiState.value.toolsBottomSheetUiState.hasUnreadComments) + } + + @Test + fun `Test assignment with no submission shows add submission`() = runTest { + val assignmentWithoutSubmission = testAssignment.copy(submission = null) + coEvery { repository.getAssignment(any(), any(), any()) } returns assignmentWithoutSubmission + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertTrue(viewModel.uiState.value.showAddSubmission) + assertFalse(viewModel.uiState.value.showSubmissionDetails) + } + + @Test + fun `Test assignment with submission shows submission details`() = runTest { + val submission = Submission(attempt = 1L, workflowState = "submitted") + val assignmentWithSubmission = testAssignment.copy( + submission = submission + ) + coEvery { repository.getAssignment(any(), any(), any()) } returns assignmentWithSubmission + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertTrue(viewModel.uiState.value.showSubmissionDetails) + } + + @Test + fun `Test LTI URL authentication is performed`() = runTest { + val ltiUrl = "https://lti.example.com/launch" + val testAssignmentWithLti = testAssignment.copy( + externalToolAttributes = mockk { + coEvery { url } returns ltiUrl + } + ) + coEvery { repository.getAssignment(any(), any(), any()) } returns testAssignmentWithLti + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertEquals(ltiUrl, viewModel.uiState.value.ltiUrl) + } + + @Test + fun `Test HTML content formatting is applied to description`() = runTest { + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + coVerify { htmlContentFormatter.formatHtmlWithIframes(testAssignment.description.orEmpty()) } + assertEquals("Formatted content", viewModel.uiState.value.instructions) + } + + @Test + fun `Test attempt selector visibility for multiple attempts`() = runTest { + val assignmentWithMultipleAttempts = testAssignment.copy(allowedAttempts = 3L) + coEvery { repository.getAssignment(any(), any(), any()) } returns assignmentWithMultipleAttempts + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertTrue(viewModel.uiState.value.toolsBottomSheetUiState.showAttemptSelector) + } + + @Test + fun `Test attempt selector is hidden for single attempt assignment`() = runTest { + val assignmentWithSingleAttempt = testAssignment.copy(allowedAttempts = 1L) + coEvery { repository.getAssignment(any(), any(), any()) } returns assignmentWithSingleAttempt + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertFalse(viewModel.uiState.value.toolsBottomSheetUiState.showAttemptSelector) + } + + @Test + fun `Test opening assignment tools updates UI state`() = runTest { + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + viewModel.openAssignmentTools() + + assertTrue(viewModel.uiState.value.toolsBottomSheetUiState.show) + } + + private fun getViewModel(savedStateHandle: SavedStateHandle): AssignmentDetailsViewModel { + return AssignmentDetailsViewModel( + context, + repository, + htmlContentFormatter, + aiAssistContextProvider, + savedStateHandle + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsRepositoryTest.kt new file mode 100644 index 0000000000..cf6e433b13 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsRepositoryTest.kt @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.moduleitemsequence.content.assignment.comments + +import com.instructure.canvasapi2.apis.SubmissionAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.managers.Comment +import com.instructure.canvasapi2.managers.CommentsData +import com.instructure.canvasapi2.managers.HorizonGetCommentsManager +import com.instructure.canvasapi2.models.Submission +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +class CommentsRepositoryTest { + private val getCommentsManager: HorizonGetCommentsManager = mockk(relaxed = true) + private val submissionApi: SubmissionAPI.SubmissionInterface = mockk(relaxed = true) + + private lateinit var repository: CommentsRepository + + private val testCommentsData = CommentsData( + comments = listOf( + Comment( + authorId = 100L, + authorName = "Student", + createdAt = Date(), + commentText = "Test comment", + read = true, + attachments = emptyList() + ) + ), + endCursor = "cursor-end", + startCursor = "cursor-start", + hasNextPage = true, + hasPreviousPage = false + ) + + private val testSubmission = Submission( + id = 1L, + attempt = 1, + userId = 100L + ) + + @Before + fun setup() { + repository = CommentsRepository(getCommentsManager, submissionApi) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getComments returns comments data successfully`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + val result = repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 1, + forceNetwork = false + ) + + assertEquals(1, result.comments.size) + assertEquals("Test comment", result.comments.first().commentText) + assertEquals("cursor-end", result.endCursor) + assertTrue(result.hasNextPage) + coVerify { getCommentsManager.getComments(1L, 100L, 1, false, false, null, null) } + } + + @Test + fun `getComments with pagination parameters`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 1, + forceNetwork = true, + startCursor = "start", + endCursor = "end", + nextPage = true + ) + + coVerify { getCommentsManager.getComments(1L, 100L, 1, true, true,"end", "start") } + } + + @Test + fun `getComments with forceNetwork true`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 1, + forceNetwork = true + ) + + coVerify { getCommentsManager.getComments(any(), any(), any(), any(), true, any(), any()) } + } + + @Test + fun `getComments for next page`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 1, + forceNetwork = false, + endCursor = "cursor-end", + nextPage = true + ) + + coVerify { getCommentsManager.getComments(any(), any(), any(), true, false, "cursor-end", null) } + } + + @Test + fun `getComments for previous page`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 1, + forceNetwork = false, + startCursor = "cursor-start", + nextPage = false + ) + + coVerify { getCommentsManager.getComments(any(), any(), any(), false, false, null, "cursor-start") } + } + + @Test + fun `postComment returns success result`() = runTest { + coEvery { submissionApi.postSubmissionComment(any(), any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testSubmission) + + val result = repository.postComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + attempt = 1, + commentText = "My comment" + ) + + assertTrue(result is DataResult.Success) + coVerify { + submissionApi.postSubmissionComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + comment = "My comment", + attemptId = 1L, + isGroupComment = false, + attachments = listOf(), + restParams = any() + ) + } + } + + @Test + fun `postComment with different attempt`() = runTest { + coEvery { submissionApi.postSubmissionComment(any(), any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testSubmission) + + repository.postComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + attempt = 5, + commentText = "Comment on attempt 5" + ) + + coVerify { + submissionApi.postSubmissionComment( + courseId = any(), + assignmentId = any(), + userId = any(), + comment = any(), + attemptId = 5L, + isGroupComment = any(), + attachments = any(), + restParams = any(), + ) + } + } + + @Test + fun `postComment returns failure result`() = runTest { + coEvery { submissionApi.postSubmissionComment(any(), any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Fail() + + val result = repository.postComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + attempt = 1, + commentText = "My comment" + ) + + assertTrue(result is DataResult.Fail) + } + + @Test + fun `getComments with attempt 0`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 0, + forceNetwork = false + ) + + coVerify { getCommentsManager.getComments(1L, 100L, 0, false, false, null, null) } + } + + @Test + fun `postComment always sets isGroupComment to false`() = runTest { + coEvery { submissionApi.postSubmissionComment(any(), any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testSubmission) + + repository.postComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + attempt = 1, + commentText = "Comment" + ) + + coVerify { + submissionApi.postSubmissionComment( + courseId = any(), + assignmentId = any(), + userId = any(), + comment = any(), + attemptId = any(), + isGroupComment = false, + attachments = any(), + restParams = any(), + ) + } + } + + @Test + fun `postComment always sends empty attachments list`() = runTest { + coEvery { submissionApi.postSubmissionComment(any(), any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testSubmission) + + repository.postComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + attempt = 1, + commentText = "Comment" + ) + + coVerify { + submissionApi.postSubmissionComment( + courseId = any(), + assignmentId = any(), + userId = any(), + comment = any(), + attemptId = 1L, + isGroupComment = any(), + attachments = emptyList(), + restParams = any(), + ) + } + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModelTest.kt new file mode 100644 index 0000000000..1f704926af --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModelTest.kt @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.moduleitemsequence.content.assignment.comments + +import android.content.Context +import android.text.format.DateFormat +import androidx.compose.ui.text.input.TextFieldValue +import androidx.work.WorkManager +import androidx.work.WorkRequest +import com.instructure.canvasapi2.managers.Comment +import com.instructure.canvasapi2.managers.CommentsData +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.horizon.R +import com.instructure.pandautils.room.appdatabase.daos.FileDownloadProgressDao +import com.instructure.pandautils.room.appdatabase.entities.FileDownloadProgressEntity +import com.instructure.pandautils.room.appdatabase.entities.FileDownloadProgressState +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class CommentsViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: CommentsRepository = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val workManager: WorkManager = mockk(relaxed = true) + private val fileDownloadProgressDao: FileDownloadProgressDao = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val assignmentId = 1L + private val courseId = 100L + private val attempt = 1 + private val userId = 123L + + private val testCommentsData = CommentsData( + comments = listOf( + Comment( + authorId = userId, + authorName = "Test User", + createdAt = Date(), + commentText = "Test comment", + read = true, + attachments = emptyList() + ) + ), + endCursor = "cursor1", + startCursor = null, + hasNextPage = true, + hasPreviousPage = false + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + mockkStatic(DateFormat::class) + every { DateFormat.is24HourFormat(any()) } returns false + every { DateFormat.getBestDateTimePattern(any(), any()) } returns "" + every { apiPrefs.user } returns User(id = userId, name = "Test User") + coEvery { repository.getComments(any(), any(), any(), any(), any(), any()) } returns testCommentsData + coEvery { repository.postComment(any(), any(), any(), any(), any()) } returns DataResult.Success(mockk(relaxed = true)) + coEvery { fileDownloadProgressDao.findByWorkerIdFlow(any()) } returns flowOf(null) + coEvery { fileDownloadProgressDao.deleteByWorkerId(any()) } returns Unit + every { workManager.enqueue(any()) } returns mockk { + every { result } returns mockk() + } + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test init with attempt loads comments`() = runTest { + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + assertFalse(viewModel.uiState.value.loading) + assertEquals(1, viewModel.uiState.value.comments.size) + assertEquals("Test comment", viewModel.uiState.value.comments.first().commentText) + coVerify { repository.getComments(assignmentId, userId, attempt, false, null, null) } + } + + @Test + fun `Test comments are mapped with user info`() = runTest { + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + val comment = viewModel.uiState.value.comments.first() + assertEquals("Test User", comment.title) + assertTrue(comment.fromCurrentUser) + assertTrue(comment.read) + } + + @Test + fun `Test paging controls visibility`() = runTest { + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + assertTrue(viewModel.uiState.value.showPagingControls) + assertTrue(viewModel.uiState.value.nextPageEnabled) + assertFalse(viewModel.uiState.value.previousPageEnabled) + } + + @Test + fun `Test load next page`() = runTest { + val nextPageData = testCommentsData.copy( + comments = listOf( + Comment( + authorId = userId, + authorName = "Test User", + createdAt = Date(), + commentText = "Next page comment", + read = true, + attachments = emptyList() + ) + ), + hasNextPage = false, + hasPreviousPage = true + ) + coEvery { repository.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData andThen nextPageData + + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + viewModel.uiState.value.onNextPageClicked() + + assertEquals("Next page comment", viewModel.uiState.value.comments.first().commentText) + coVerify { repository.getComments(assignmentId, userId, attempt, any(), null, "cursor1", any()) } + } + + @Test + fun `Test load previous page`() = runTest { + val firstPageData = testCommentsData.copy(hasPreviousPage = true) + coEvery { repository.getComments(any(), any(), any(), any(), any(), any()) } returns firstPageData andThen testCommentsData + + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + viewModel.uiState.value.onPreviousPageClicked() + + coVerify { repository.getComments(assignmentId, userId, attempt, false, null, null) } + } + + @Test + fun `Test comment text change updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onCommentChanged(TextFieldValue("New comment")) + + assertEquals("New comment", viewModel.uiState.value.comment.text) + } + + @Test + fun `Test post comment with valid text`() { + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + viewModel.uiState.value.onCommentChanged(TextFieldValue("New comment")) + + viewModel.uiState.value.onPostClicked() + + coVerify { repository.postComment(courseId, assignmentId, userId, attempt, "New comment") } + assertEquals("", viewModel.uiState.value.comment.text) + assertFalse(viewModel.uiState.value.postingComment) + } + + @Test + fun `Test post comment with blank text does nothing`() = runTest { + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + viewModel.uiState.value.onPostClicked() + + coVerify(exactly = 0) { repository.postComment(any(), any(), any(), any(), any()) } + } + + @Test + fun `Test post comment error shows message`() = runTest { + coEvery { repository.postComment(any(), any(), any(), any(), any()) } throws Exception("Post error") + + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + viewModel.uiState.value.onCommentChanged(TextFieldValue("New comment")) + + viewModel.uiState.value.onPostClicked() + + assertNotNull(viewModel.uiState.value.errorMessage) + assertFalse(viewModel.uiState.value.postingComment) + } + + @Test + fun `Test error dismissed clears message`() = runTest { + coEvery { repository.getComments(any(), any(), any(), any(), any(), any()) } throws Exception("Load error") + + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + viewModel.uiState.value.onErrorDismissed() + + assertNull(viewModel.uiState.value.errorMessage) + } + + @Test + fun `Test file opened clears file path`() = runTest { + val completedEntity = FileDownloadProgressEntity( + workerId = "worker-id", + progressState = FileDownloadProgressState.COMPLETED, + progress = 100, + filePath = "/path/to/file", + fileName = "fileName" + ) + coEvery { fileDownloadProgressDao.findByWorkerIdFlow(any()) } returns flowOf(completedEntity) + + val viewModel = getViewModel() + + viewModel.uiState.value.onFileOpened() + + assertNull(viewModel.uiState.value.filePathToOpen) + assertNull(viewModel.uiState.value.mimeTypeToOpen) + } + + @Test + fun `Test load comments error shows message`() = runTest { + coEvery { repository.getComments(any(), any(), any(), any(), any(), any()) } throws Exception("Load error") + + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + assertFalse(viewModel.uiState.value.loading) + assertNotNull(viewModel.uiState.value.errorMessage) + } + + @Test + fun `Test attempt 0 shows no subtitle`() = runTest { + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, 0, courseId) + + val comment = viewModel.uiState.value.comments.first() + assertEquals("", comment.subtitle) + } + + @Test + fun `Test non-zero attempt shows subtitle`() = runTest { + every { context.getString(R.string.commentsBottomSheet_attempt, any()) } returns "Attempt $attempt" + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, 2, courseId) + + val comment = viewModel.uiState.value.comments.first() + assertTrue(comment.subtitle.isNotEmpty()) + } + + @Test + fun `Test comment from different user`() = runTest { + val otherUserComment = testCommentsData.copy( + comments = listOf( + Comment( + authorId = 999L, + authorName = "Other User", + createdAt = Date(), + commentText = "Other comment", + read = false, + attachments = emptyList() + ) + ) + ) + coEvery { repository.getComments(any(), any(), any(), any(), any(), any(), any()) } returns otherUserComment + + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + val comment = viewModel.uiState.value.comments.first() + assertFalse(comment.fromCurrentUser) + assertFalse(comment.read) + } + + private fun getViewModel(): CommentsViewModel { + return CommentsViewModel( + context, + repository, + apiPrefs, + workManager, + fileDownloadProgressDao + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsRepositoryTest.kt new file mode 100644 index 0000000000..b42d2baaf1 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsRepositoryTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.moduleitemsequence.content.page + +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.managers.RedwoodApiManager +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.redwood.QueryNotesQuery +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +class PageDetailsRepositoryTest { + private val pageApi: PageAPI.PagesInterface = mockk(relaxed = true) + private val oAuthInterface: OAuthAPI.OAuthInterface = mockk(relaxed = true) + private val redwoodApi: RedwoodApiManager = mockk(relaxed = true) + + private lateinit var repository: PageDetailsRepository + + private val testPage = Page( + id = 1L, + url = "test-page", + title = "Test Page", + body = "

Page content

" + ) + + private val testNotes = QueryNotesQuery.Notes( + pageInfo = QueryNotesQuery.PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = null, + endCursor = null + ), + edges = listOf( + QueryNotesQuery.Edge( + cursor = "", + node = QueryNotesQuery.Node( + id = "1", + objectId = "1", + objectType = "Page", + userText = "comment 1", + rootAccountUuid = "1", + userId = "1", + courseId = "1", + reaction = listOf("Important"), + highlightData = "", + createdAt = Date(), + updatedAt = Date(), + ) + ), + QueryNotesQuery.Edge( + cursor = "", + node = QueryNotesQuery.Node( + id = "2", + objectId = "1", + objectType = "Page", + userText = "comment 2", + rootAccountUuid = "1", + userId = "1", + courseId = "1", + reaction = listOf("Important"), + highlightData = "", + createdAt = Date(), + updatedAt = Date(), + ) + ) + ) + ) + + @Before + fun setup() { + repository = PageDetailsRepository(pageApi, oAuthInterface, redwoodApi) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getPageDetails returns page successfully`() = runTest { + coEvery { pageApi.getDetailedPage(any(), any(), any(), any()) } returns DataResult.Success(testPage) + + val result = repository.getPageDetails(courseId = 1L, pageId = "test-page", forceNetwork = false) + + assertEquals("Test Page", result.title) + assertEquals("

Page content

", result.body) + coVerify { pageApi.getDetailedPage("courses", 1L, "test-page", any()) } + } + + @Test + fun `getPageDetails with forceNetwork true`() = runTest { + coEvery { pageApi.getDetailedPage(any(), any(), any(), any()) } returns DataResult.Success(testPage) + + repository.getPageDetails(courseId = 1L, pageId = "test-page", forceNetwork = true) + + coVerify { pageApi.getDetailedPage(any(), any(), any(), match { it.isForceReadFromNetwork }) } + } + + @Test + fun `getPageDetails with forceNetwork false`() = runTest { + coEvery { pageApi.getDetailedPage(any(), any(), any(), any()) } returns DataResult.Success(testPage) + + repository.getPageDetails(courseId = 1L, pageId = "test-page", forceNetwork = false) + + coVerify { pageApi.getDetailedPage(any(), any(), any(), match { !it.isForceReadFromNetwork }) } + } + + @Test + fun `authenticateUrl returns authenticated URL`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://authenticated.url") + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + val result = repository.authenticateUrl("https://example.com/page") + + assertEquals("https://authenticated.url", result) + coVerify { oAuthInterface.getAuthenticatedSession("https://example.com/page", any()) } + } + + @Test + fun `authenticateUrl returns original URL on failure`() = runTest { + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Fail() + + val result = repository.authenticateUrl("https://example.com/page") + + assertEquals("https://example.com/page", result) + } + + @Test + fun `authenticateUrl returns original URL when session URL is null`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://example.com/page/authenticated") + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + val result = repository.authenticateUrl("https://example.com/page") + + assertEquals("https://example.com/page/authenticated", result) + } + + @Test + fun `getNotes returns notes list`() = runTest { + coEvery { redwoodApi.getNotes(any(), any(), any()) } returns testNotes + + val result = repository.getNotes(courseId = 1L, pageId = 100L) + + assertEquals(2, result.size) + assertEquals("comment 1", result.first().userText) + coVerify { redwoodApi.getNotes(any(), null, null) } + } + + @Test + fun `getNotes with different page ID`() = runTest { + coEvery { redwoodApi.getNotes(any(), any(), any()) } returns testNotes + + repository.getNotes(courseId = 5L, pageId = 200L) + + coVerify { redwoodApi.getNotes(any(), null, null) } + } + + @Test + fun `getNotes returns empty list`() = runTest { + coEvery { redwoodApi.getNotes(any(), any(), any()) } returns QueryNotesQuery.Notes( + pageInfo = QueryNotesQuery.PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = null, + endCursor = null + ), + edges = emptyList() + ) + + val result = repository.getNotes(courseId = 1L, pageId = 100L) + + assertEquals(0, result.size) + } + + @Test + fun `authenticateUrl always uses forceNetwork`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://authenticated.url") + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + repository.authenticateUrl("https://example.com") + + coVerify { oAuthInterface.getAuthenticatedSession(any(), match { it.isForceReadFromNetwork }) } + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsViewModelTest.kt new file mode 100644 index 0000000000..06627fc3d1 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsViewModelTest.kt @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.moduleitemsequence.content.page + +import androidx.lifecycle.SavedStateHandle +import com.instructure.canvasapi2.managers.NoteHighlightedData +import com.instructure.canvasapi2.managers.NoteHighlightedDataRange +import com.instructure.canvasapi2.managers.NoteHighlightedDataTextPosition +import com.instructure.canvasapi2.managers.NoteObjectType +import com.instructure.canvasapi2.models.Page +import com.instructure.horizon.features.moduleitemsequence.ModuleItemContent +import com.instructure.horizon.features.notebook.addedit.add.AddNoteRepository +import com.instructure.horizon.features.notebook.common.model.Note +import com.instructure.horizon.features.notebook.common.model.NotebookType +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.HtmlContentFormatter +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class PageDetailsViewModelTest { + private val repository: PageDetailsRepository = mockk(relaxed = true) + private val htmlContentFormatter: HtmlContentFormatter = mockk(relaxed = true) + private val addNoteRepository: AddNoteRepository = mockk(relaxed = true) + private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val courseId = 1L + private val pageUrl = "test-page" + private val testPage = Page( + id = 100L, + url = pageUrl, + title = "Test Page", + body = "

Test content

" + ) + + private val testNotes = listOf( + Note( + id = "1", + objectId = "1", + objectType = NoteObjectType.PAGE, + userText = "comment 1", + highlightedText = NoteHighlightedData( + selectedText = "highlighted text 1", + range = NoteHighlightedDataRange(1, 5, "start", "end"), + textPosition = NoteHighlightedDataTextPosition(1, 5) + ), + type = NotebookType.Important, + updatedAt = Date(), + courseId = 1, + ), + Note( + id = "2", + objectId = "1", + objectType = NoteObjectType.PAGE, + userText = "comment 2", + highlightedText = NoteHighlightedData( + selectedText = "highlighted text 2", + range = NoteHighlightedDataRange(10, 15, "start", "end"), + textPosition = NoteHighlightedDataTextPosition(10, 15) + ), + type = NotebookType.Confusing, + updatedAt = Date(), + courseId = 1, + ) + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + every { savedStateHandle.get(Const.COURSE_ID) } returns courseId + every { savedStateHandle.get(ModuleItemContent.Page.PAGE_URL) } returns pageUrl + coEvery { repository.getPageDetails(any(), any()) } returns testPage + coEvery { repository.getNotes(any(), any()) } returns testNotes + coEvery { repository.authenticateUrl(any()) } returns "https://authenticated.url" + coEvery { htmlContentFormatter.formatHtmlWithIframes(any()) } answers { firstArg() } + coEvery { addNoteRepository.addNote(any(), any(), any(), any(), any(), any()) } returns Unit + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel loads page details`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertEquals("

Test content

", viewModel.uiState.value.pageHtmlContent) + assertEquals(100L, viewModel.uiState.value.pageId) + assertEquals(pageUrl, viewModel.uiState.value.pageUrl) + coVerify { repository.getPageDetails(courseId, pageUrl) } + } + + @Test + fun `Test HTML content is formatted`() = runTest { + coEvery { htmlContentFormatter.formatHtmlWithIframes(any()) } returns "formatted html" + + val viewModel = getViewModel() + + assertEquals("formatted html", viewModel.uiState.value.pageHtmlContent) + coVerify { htmlContentFormatter.formatHtmlWithIframes("

Test content

") } + } + + @Test + fun `Test notes are loaded`() = runTest { + val viewModel = getViewModel() + + assertEquals(2, viewModel.uiState.value.notes.size) + assertEquals("comment 1", viewModel.uiState.value.notes.first().userText) + coVerify { repository.getNotes(courseId, 100L) } + } + + @Test + fun `Test notes loading failure does not fail page load`() = runTest { + coEvery { repository.getNotes(any(), any()) } throws Exception("Notes error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertTrue(viewModel.uiState.value.notes.isEmpty()) + assertNotNull(viewModel.uiState.value.pageHtmlContent) + } + + @Test + fun `Test LTI button pressed authenticates URL`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.ltiButtonPressed?.invoke("https://lti.url") + + assertEquals("https://authenticated.url", viewModel.uiState.value.urlToOpen) + coVerify { repository.authenticateUrl("https://lti.url") } + } + + @Test + fun `Test LTI authentication failure returns original URL`() = runTest { + coEvery { repository.authenticateUrl(any()) } throws Exception("Auth error") + + val viewModel = getViewModel() + + viewModel.uiState.value.ltiButtonPressed?.invoke("https://lti.url") + + assertEquals("https://lti.url", viewModel.uiState.value.urlToOpen) + } + + @Test + fun `Test URL opened clears URL to open`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.ltiButtonPressed?.invoke("https://lti.url") + + viewModel.uiState.value.onUrlOpened() + + assertNull(viewModel.uiState.value.urlToOpen) + } + + @Test + fun `Test add note creates note and refreshes`() = runTest { + val viewModel = getViewModel() + val highlightedData = NoteHighlightedData( + selectedText = "highlighted text", + range = NoteHighlightedDataRange(1, 5, "start", "end"), + textPosition = NoteHighlightedDataTextPosition(1, 5) + ) + + viewModel.uiState.value.addNote(highlightedData, "Important") + + coVerify { addNoteRepository.addNote( + courseId = courseId.toString(), + objectId = testPage.id.toString(), + objectType = "Page", + highlightedData = highlightedData, + userComment = "", + type = NotebookType.Important + ) } + + coVerify(atLeast = 2) { repository.getNotes(courseId, 100L) } + } + + @Test + fun `Test refresh notes updates state`() = runTest { + val updatedNotes = testNotes + testNotes.last().copy(userText = "New note") + coEvery { repository.getNotes(any(), any()) } returns testNotes andThen updatedNotes + + val viewModel = getViewModel() + assertEquals(2, viewModel.uiState.value.notes.size) + + viewModel.refreshNotes() + + assertEquals(3, viewModel.uiState.value.notes.size) + assertEquals("New note", viewModel.uiState.value.notes.last().userText) + } + + @Test + fun `Test refresh notes handles error`() = runTest { + val viewModel = getViewModel() + coEvery { repository.getNotes(any(), any()) } throws Exception("Error") + + // Should not crash + viewModel.refreshNotes() + } + + @Test + fun `Test page load error sets error state`() = runTest { + coEvery { repository.getPageDetails(any(), any()) } throws Exception("Error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertTrue(viewModel.uiState.value.loadingState.isError) + } + + @Test + fun `Test course ID is set in UI state`() = runTest { + val viewModel = getViewModel() + + assertEquals(courseId, viewModel.uiState.value.courseId) + } + + private fun getViewModel(): PageDetailsViewModel { + return PageDetailsViewModel( + repository, + htmlContentFormatter, + addNoteRepository, + savedStateHandle + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookRepositoryTest.kt new file mode 100644 index 0000000000..c641278a5e --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookRepositoryTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.notebook + +import com.apollographql.apollo.api.Optional +import com.instructure.canvasapi2.managers.RedwoodApiManager +import com.instructure.horizon.features.notebook.common.model.NotebookType +import com.instructure.redwood.QueryNotesQuery +import com.instructure.redwood.type.LearningObjectFilter +import com.instructure.redwood.type.NoteFilterInput +import com.instructure.redwood.type.OrderDirection +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class NotebookRepositoryTest { + private val redwoodApiManager: RedwoodApiManager = mockk(relaxed = true) + + @Test + fun `Test successful notes retrieval with filter`() = runTest { + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = false, endCursor = null, startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + val result = getRepository().getNotes( + filterType = NotebookType.Important, + courseId = 1L, + orderDirection = OrderDirection.descending + ) + + assertNotNull(result) + assertEquals(mockNotes, result) + } + + @Test + fun `Test notes retrieval with pagination after cursor`() = runTest { + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = true, hasPreviousPage = false, endCursor = "cursor123", startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(after = "cursor123") + + coVerify { redwoodApiManager.getNotes(filter = any(), firstN = 10, lastN = any(), after = "cursor123", before = any(), orderBy = any()) } + } + + @Test + fun `Test notes retrieval with pagination before cursor`() = runTest { + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = true, endCursor = null, startCursor = "cursor456") + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(before = "cursor456") + + coVerify { redwoodApiManager.getNotes(filter = any(), firstN = any(), lastN = 10, after = any(), before = "cursor456", orderBy = any()) } + } + + @Test + fun `Test notes retrieval with course filter`() = runTest { + val courseId = 123L + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = false, endCursor = null, startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(courseId = courseId) + + coVerify { redwoodApiManager.getNotes(NoteFilterInput( + courseId = Optional.present(courseId.toString()), + ), any(), any(), any(), any(), any()) } + } + + @Test + fun `Test notes retrieval with learning object filter`() = runTest { + val objectTypeAndId = Pair("Assignment", "456") + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = false, endCursor = null, startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(objectTypeAndId = objectTypeAndId) + + coVerify { redwoodApiManager.getNotes(NoteFilterInput( + learningObject = Optional.present(LearningObjectFilter( + type = objectTypeAndId.first, + id = objectTypeAndId.second + )) + ), any(), any(), any(), any(), any()) } + } + + @Test + fun `Test notes retrieval with custom item count`() = runTest { + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = false, endCursor = null, startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(itemCount = 25) + + coVerify { redwoodApiManager.getNotes(filter = any(), firstN = 25, lastN = any(), after = any(), before = any(), orderBy = any()) } + } + + @Test + fun `Test notes retrieval with reaction filter`() = runTest { + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = false, endCursor = null, startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(filterType = NotebookType.Confusing) + + coVerify { redwoodApiManager.getNotes(NoteFilterInput( + reactions = Optional.present(listOf("Confusing")) + ), any(), any(), any(), any(), any()) } + } + + private fun getRepository(): NotebookRepository { + return NotebookRepository(redwoodApiManager) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookViewModelTest.kt new file mode 100644 index 0000000000..2a2ab3ced0 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookViewModelTest.kt @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2025 - 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.horizon.features.notebook + +import com.instructure.horizon.features.notebook.common.model.NotebookType +import com.instructure.redwood.QueryNotesQuery +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class NotebookViewModelTest { + private val repository: NotebookRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testNotes = QueryNotesQuery.Notes( + edges = listOf( + QueryNotesQuery.Edge( + cursor = "cursor1", + node = QueryNotesQuery.Node( + id = "note1", + userText = "Test note 1", + createdAt = Date(), + updatedAt = Date(), + rootAccountUuid = "", + userId = "1", + courseId = "1", + objectId = "1", + objectType = "Assignment", + reaction = listOf(""), + highlightData = "test" + ) + ) + ), + pageInfo = QueryNotesQuery.PageInfo( + hasNextPage = true, + hasPreviousPage = false, + endCursor = "endCursor1", + startCursor = "startCursor1" + ) + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getNotes(any(), any(), any(), any(), any(), any(), any()) } returns testNotes + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads successfully on init`() { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.isLoading) + coVerify { repository.getNotes(any(), any(), any(), any(), any(), any(), any()) } + } + + @Test + fun `Test notes are loaded`() { + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.notes.isNotEmpty()) + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getNotes(any(), any(), any(), any(), any(), any(), any()) } throws Exception("Network error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.isLoading) + assertTrue(viewModel.uiState.value.notes.isEmpty()) + } + + @Test + fun `Test pagination info is updated`() { + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.hasNextPage) + assertFalse(viewModel.uiState.value.hasPreviousPage) + } + + @Test + fun `Test filter selection updates state and reloads data`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFilterSelected(NotebookType.Important) + + assertEquals(NotebookType.Important, viewModel.uiState.value.selectedFilter) + coVerify(atLeast = 2) { repository.getNotes(any(), any(), any(), any(), any(), any(), any()) } + } + + @Test + fun `Test filter selection with null clears filter`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFilterSelected(null) + + assertEquals(null, viewModel.uiState.value.selectedFilter) + } + + @Test + fun `Test load next page uses end cursor`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.loadNextPage() + + coVerify { repository.getNotes(after = "endCursor1", before = null, any(), any(), any(), any(), any()) } + } + + @Test + fun `Test load previous page uses start cursor`() = runTest { + val notesWithPrevious = testNotes.copy( + pageInfo = testNotes.pageInfo.copy(hasPreviousPage = true) + ) + coEvery { repository.getNotes(any(), any(), any(), any(), any(), any(), any()) } returns notesWithPrevious + + val viewModel = getViewModel() + + viewModel.uiState.value.loadPreviousPage() + + coVerify { repository.getNotes(after = null, before = "startCursor1", any(), any(), any(), any(), any()) } + } + + @Test + fun `Test update course id reloads data`() = runTest { + val viewModel = getViewModel() + + viewModel.updateCourseId(123L) + viewModel.updateCourseId(1234L) + viewModel.updateCourseId(123L) + + coVerify(exactly = 2) { repository.getNotes(any(), any(), any(), any(), 123L, any(), any()) } + } + + @Test + fun `Test update content with course id hides top bar`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateContent(123L, null) + + assertFalse(viewModel.uiState.value.showTopBar) + assertTrue(viewModel.uiState.value.showFilters) + } + + @Test + fun `Test update content with object type hides filters`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateContent(123L, Pair("Assignment", "456")) + + assertFalse(viewModel.uiState.value.showTopBar) + assertFalse(viewModel.uiState.value.showFilters) + } + + @Test + fun `Test update content without course id shows top bar and filters`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateContent(null, null) + + assertTrue(viewModel.uiState.value.showTopBar) + assertTrue(viewModel.uiState.value.showFilters) + } + + private fun getViewModel(): NotebookViewModel { + return NotebookViewModel(repository) + } +} From e0b437adc83027546ffd4400c662064746b392a8 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Wed, 8 Oct 2025 09:33:50 +0200 Subject: [PATCH 27/94] [MBL-19357][All] - Eliminate Splunk and basic setup for Observe (#3288) * Migrate test report logs from Splunk to Observe. refs: MBL-19357 affects: Student, Teacher, Parent release note: * Change Observe URL. refs: MBL-19357 affects: Student, Teacher, Parent release note: * Change 'event' json attribute to 'data'. refs: MBL-19357 affects: Student, Teacher, Parent release note: * Simplify JSON (cut sourceType from it as Observe only expects a data). Define content-length to make sure it's a json type. refs: MBL-19357 affects: Student, Teacher, Parent release note: * Refactor bash script to send data to Observe in correct form. refs: MBL-19357 affects: Student, Teacher, Parent release note: * Migrate from Splunk to Observe in TimingsListener (measuring timing, size, etc.) refs: MBL-19357 affects: Student, Teacher, Parent release note: --- .../src/main/groovy/TimingsListener.groovy | 24 +++---- apps/postProcessTestRun.bash | 64 ++++++++++++------- .../instructure/canvas/espresso/CanvasTest.kt | 30 +++++---- 3 files changed, 70 insertions(+), 48 deletions(-) diff --git a/apps/buildSrc/src/main/groovy/TimingsListener.groovy b/apps/buildSrc/src/main/groovy/TimingsListener.groovy index 7d14d278c8..fcef0f946e 100644 --- a/apps/buildSrc/src/main/groovy/TimingsListener.groovy +++ b/apps/buildSrc/src/main/groovy/TimingsListener.groovy @@ -21,7 +21,6 @@ class TimingsListener implements TaskExecutionListener, BuildListener { buildStartTime = System.nanoTime() } - @Override void beforeExecute(Task task) { startTime = System.nanoTime() @@ -39,8 +38,8 @@ class TimingsListener implements TaskExecutionListener, BuildListener { // Compute build time def totalBuildTimeMs = TimeUnit.MILLISECONDS.convert(System.nanoTime() - buildStartTime, TimeUnit.NANOSECONDS) - // Grab the Splunk-mobile token from Bitrise - def splunkToken = System.getenv("SPLUNK_MOBILE_TOKEN") + // Grab the Observe-mobile token from Bitrise + def observeToken = System.getenv("OBSERVE_MOBILE_TOKEN") // Let's abort early if (1) the build failed, or (2) we're not on bitrise if(result.failure != null) { @@ -48,7 +47,7 @@ class TimingsListener implements TaskExecutionListener, BuildListener { return } - if(splunkToken == null || splunkToken.isEmpty()) { + if(observeToken == null || observeToken.isEmpty()) { println("Build report logic aborting because we're not on bitrise") return } @@ -118,8 +117,7 @@ class TimingsListener implements TaskExecutionListener, BuildListener { } println("file name=${file.path} length=${file.length()}") - - // Construct the JSON payload for our "buildComplete" event + // Construct the JSON eventPayload for our "buildComplete" event def payloadBuilder = new groovy.json.JsonBuilder() payloadBuilder buildTime: totalBuildTimeMs, gradleTasks: startTaskNames, @@ -131,16 +129,20 @@ class TimingsListener implements TaskExecutionListener, BuildListener { bitriseBuildNumber: bitriseBuildNumber, topTasks: top10 - // Create the event payload. Change key/value in top 10 tasks to task/ms. - def payload = payloadBuilder.toString().replaceAll("\"key\"", "\"task\"").replaceAll("\"value\"", "\"ms\"") + // Create the event eventPayload. Change key/value in top 10 tasks to task/ms. + def eventPayload = payloadBuilder.toString().replaceAll("\"key\"", "\"task\"").replaceAll("\"value\"", "\"ms\"") + + println("event eventPayload: $eventPayload") - println("event payload: $payload") + //Wrap it in the "data" object for Observe + def payload="{\"data\": $eventPayload}" // Let's issue our curl command to emit our data refProject.exec { executable "curl" - args "-k", "https://http-inputs-inst.splunkcloud.com:443/services/collector", "-H", "Authorization: Splunk $splunkToken", - "-d", "{\"sourcetype\" : \"mobile-android-build\", \"event\" : $payload}" + args "-k", "https://103443579803.collect.observeinc.com/v1/http", "-H", "Authorization: Bearer $observeToken", + "-H", "Content-Type: application/json", + "-d", "$payload" } } diff --git a/apps/postProcessTestRun.bash b/apps/postProcessTestRun.bash index 83d41dc037..edddc58de8 100755 --- a/apps/postProcessTestRun.bash +++ b/apps/postProcessTestRun.bash @@ -4,8 +4,8 @@ set -e # debug log # set -x -# Script post-process our test results and reports the results to Splunk. -# Only works on Bitrise, as certain secrets (like the Splunk token) will not be defined locally. +# Script post-process our test results and reports the results to Observe. +# Only works on Bitrise, as certain secrets (like the Observe token) will not be defined locally. # Capture our command line arguments if [[ $# < 2 ]] @@ -26,14 +26,18 @@ suiteName="" # Common JSON parameters for all event types commonData="\"workflow\" : \"$BITRISE_TRIGGERED_WORKFLOW_ID\", \"app\" : \"$appName\", \"branch\" : \"$BITRISE_GIT_BRANCH\"" -# A running collection of info for all passed tests. JSON object strings are just concatenated together. +# A running collection of info for all passed tests. We'll use Newline Delimited JSON (NDJSON). successReport="" successCount=0 -# Emits collected successful test data to splunk, and zeroes out the running trackers. +# Emits collected successful test data to Observe, and zeroes out the running trackers. emitSuccessfulTestData () { + # Bails if there's nothing to report + if [[ -z "$successReport" ]]; then return; fi + #echo -e "\nSuccess payload: $successReport\n" - curl -k "https://http-inputs-inst.splunkcloud.com:443/services/collector" -H "Authorization: Splunk $SPLUNK_MOBILE_TOKEN" -d "$successReport" + # Post the data as newline-delimited JSON + curl -k "https://103443579803.collect.observeinc.com/v1/http" -H "Authorization: Bearer $OBSERVE_MOBILE_TOKEN" -H "Content-Type: application/x-ndjson" --data-binary @- <<< "$successReport" successReport="" # Reset the successReport after emitting it successCount=0 } @@ -42,31 +46,32 @@ emitSuccessfulTestData () { while IFS= read -r line do # For lines, emit a deviceSummary event and remember the suiteName - # Sample line: + # Sample line: if [[ $line =~ "testsuite name" ]] then suiteName=`echo $line | cut -d " " -f 2 | cut -d = -f 2` numTests=`echo $line | cut -d " " -f 3 | cut -d = -f 2 | tr -d '"'` numFailures=`echo $line | cut -d " " -f 4 | cut -d = -f 2 | tr -d '"'` runTime=`echo $line | cut -d " " -f 7 | cut -d = -f 2 | tr -d '"'` - - payload="{\"deviceConfig\" : $suiteName, \"numTests\" : $numTests, \"numFailures\" : $numFailures, \"runTime\" : $runTime, $commonData}" + + # This is the event data + eventPayload="{\"deviceConfig\" : $suiteName, \"numTests\" : $numTests, \"numFailures\" : $numFailures, \"runTime\" : $runTime, $commonData}" + # Wrap it in the "data" object for Observe + payload="{\"data\": $eventPayload}" echo -e "\nsummary payload: $payload" - curl -k "https://http-inputs-inst.splunkcloud.com:443/services/collector" -H "Authorization: Splunk $SPLUNK_MOBILE_TOKEN" -d "{\"sourcetype\" : \"mobile-android-qa-summary\", \"event\" : $payload}" + curl -k "https://103443579803.collect.observeinc.com/v1/http" -H "Authorization: Bearer $OBSERVE_MOBILE_TOKEN" -H "Content-Type: application/json" -d "$payload" fi - # For lines, create a "test passed" payload. We won't include it in our "successReport" until we've - # verified that the test didn't fail. - # Sample line: + # For lines, store the test info. We will construct the payload at + # Sample line: if [[ $line =~ "testcase name" ]] then # Remove the '<' and '>' from around the line line=`echo $line | tr -d "<>"` # Extract various fields from the line - testName=`echo $line | cut -d " " -f 2 | cut -d = -f 2` - className=`echo $line | cut -d " " -f 3 | cut -d = -f 2` - runTime=`echo $line | cut -d " " -f 4 | cut -d = -f 2 | tr -d '"'` - payload="{\"sourcetype\" : \"mobile-android-qa-testresult\", \"event\" : {\"buildUrl\" : \"$BITRISE_BUILD_URL\", \"status\" : \"passed\", \"testName\": $testName, \"testClass\" : $className, \"deviceConfig\" : $suiteName, \"runTime\" : $runTime, $commonData}}" + currentTestName=`echo $line | cut -d " " -f 2 | cut -d = -f 2` + currentClassName=`echo $line | cut -d " " -f 3 | cut -d = -f 2` + currentRunTime=`echo $line | cut -d " " -f 4 | cut -d = -f 2 | tr -d '"'` failureEncountered=false fi @@ -76,19 +81,26 @@ do failureEncountered=true fi - # If we get to the end of a testcase and no failure has been recorded, then include the test info + # If we get to the end of a testcase and no failure has been recorded, then include the test info # in our "successReport". if [[ $line =~ "" ]] then if [[ $failureEncountered = false ]] then - successReport="$successReport $payload" + # Construct the event payload for the passed test + eventPayload="{\"buildUrl\" : \"$BITRISE_BUILD_URL\", \"status\" : \"passed\", \"testName\": $currentTestName, \"testClass\" : $currentClassName, \"deviceConfig\" : $suiteName, \"runTime\" : $currentRunTime, $commonData}" + # Wrap it in the "data" object for Observe + payload="{\"data\": $eventPayload}" + + # Append the full JSON object with a newline for NDJSON format + successReport="${successReport}${payload}"$'\n' ((successCount=successCount+1)) - # Emit successful test data to Splunk every 100 tests + + # Emit successful test data to Observe every 100 tests if [ $successCount -eq 100 ] then emitSuccessfulTestData - fi + fi fi fi done < "$reportFile" @@ -138,9 +150,13 @@ do totalMinutes=$((hours * 60 + minutes)) #echo "totalMinutes: $totalMinutes" - payload="{\"minutes\" : $totalMinutes, \"cost\" : $cost, $commonData}" + + # This is the event data + eventPayload="{\"minutes\" : $totalMinutes, \"cost\" : $cost, $commonData}" + # Wrap it in the "data" object for Observe + payload="{\"data\": $eventPayload}" + echo -e "\ncost payload: $payload" - #curl -X POST -H "Content-Type: application/json" -d "$payload" $SUMOLOGIC_ENDPOINT - curl -k "https://http-inputs-inst.splunkcloud.com:443/services/collector" -H "Authorization: Splunk $SPLUNK_MOBILE_TOKEN" -d "{\"sourcetype\" : \"mobile-android-qa-cost\", \"event\" : $payload}" + curl -k "https://103443579803.collect.observeinc.com/v1/http" -H "Authorization: Bearer $OBSERVE_MOBILE_TOKEN" -H "Content-Type: application/json" -d "$payload" fi -done < "$costFile" +done < "$costFile" \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt index d5d98c264a..4b76f23405 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt @@ -138,24 +138,24 @@ abstract class CanvasTest : InstructureTestingContract { Log.d("TEST RETRY", "testMethod: $testMethod, error=$error, stacktrace=${error.stackTrace.joinToString("\n")} cause=${error.cause}") } - // Grab the Splunk-mobile token from Bitrise - val splunkToken = InstrumentationRegistry.getArguments().getString("SPLUNK_MOBILE_TOKEN") + // Grab the Observe-mobile token from Bitrise + val observeToken = InstrumentationRegistry.getArguments().getString("OBSERVE_MOBILE_TOKEN") // Only continue if we're on Bitrise // (More accurately, if we are on FTL launched from Bitrise.) - if(!splunkToken.isNullOrEmpty()) { - val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + if(!observeToken.isNullOrEmpty()) { + val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) val hasActiveNetwork = networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false if (hasActiveNetwork) { - reportToSplunk(disposition, testMethod, testClass, error, splunkToken) + reportToObserve(disposition, testMethod, testClass, error, observeToken) } else { turnOnConnectionViaADB() connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { super.onAvailable(network) connectivityManager.unregisterNetworkCallback(this) - reportToSplunk(disposition, testMethod, testClass, error, splunkToken) + reportToObserve(disposition, testMethod, testClass, error, observeToken) } }) } @@ -168,12 +168,12 @@ abstract class CanvasTest : InstructureTestingContract { } - private fun reportToSplunk( + private fun reportToObserve( disposition: String, testMethod: String, testClass: String, error: Throwable, - splunkToken: String? + observeToken: String? ) { val bitriseWorkflow = InstrumentationRegistry.getArguments().getString("BITRISE_TRIGGERED_WORKFLOW_ID") @@ -186,21 +186,24 @@ abstract class CanvasTest : InstructureTestingContract { eventObject.put("workflow", bitriseWorkflow) eventObject.put("branch", bitriseBranch) eventObject.put("bitriseApp", bitriseApp) + eventObject.put("buildNumber", bitriseBuildNumber) eventObject.put("status", disposition) eventObject.put("testName", testMethod) eventObject.put("testClass", testClass) eventObject.put("stackTrace", error.stackTrace.take(15).joinToString(", ")) eventObject.put("osVersion", Build.VERSION.SDK_INT.toString()) + eventObject.put("sourcetype", "mobile-android-qa-testresult") // Limit our error message to 4096 chars; they can be unreasonably long (e.g., 137K!) when // they contain a view hierarchy, and there is typically not much useful info after the // first few lines. eventObject.put("message", error.toString().take(4096)) val payloadObject = JSONObject() - payloadObject.put("sourcetype", "mobile-android-qa-testresult") - payloadObject.put("event", eventObject) + + payloadObject.put("data", eventObject) val payload = payloadObject.toString() + val payloadBytes = payload.toByteArray(Charsets.UTF_8) Log.d("CanvasTest", "payload = $payload") // Can't run a curl command from FTL, so let's do this the hard way @@ -211,11 +214,12 @@ abstract class CanvasTest : InstructureTestingContract { try { // Set up our url/connection - val url = URL("https://http-inputs-inst.splunkcloud.com:443/services/collector") + val url = URL("https://103443579803.collect.observeinc.com/v1/http") conn = url.openConnection() as HttpURLConnection conn.requestMethod = "POST" - conn.setRequestProperty("Authorization", "Splunk $splunkToken") + conn.setRequestProperty("Authorization", "Bearer $observeToken") conn.setRequestProperty("Content-Type", "application/json; utf-8") + conn.setRequestProperty("Content-Length", payloadBytes.size.toString()) conn.setRequestProperty("Accept", "application/json") conn.setDoInput(true) conn.setDoOutput(true) @@ -234,7 +238,7 @@ abstract class CanvasTest : InstructureTestingContract { "Response code: ${conn.responseCode}, message: ${conn.responseMessage}" ) - // Report the splunk result JSON + // Report the Observe result JSON inputStream = conn.inputStream val content = inputStream.bufferedReader().use(BufferedReader::readText) Log.d("CanvasTest", "Response: $content") From 9a2d05a124b486fb1591d674534b7d09f81401eb Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Wed, 8 Oct 2025 10:15:16 +0200 Subject: [PATCH 28/94] [MBL-19278][Teacher] Assignment List page doesn't refresh automatically anymore refs: MBL-19278 affects: Teacher release note: Added automatic refresh to Assignment List when grading submissions in SpeedGrader. --- .../list/AssignmentListFragment.kt | 22 +++++++++++++++++++ .../list/AssignmentListViewModel.kt | 6 ++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListFragment.kt index 41a4baf7b8..c00b9ea02d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListFragment.kt @@ -39,12 +39,16 @@ import com.instructure.pandautils.analytics.SCREEN_VIEW_ASSIGNMENT_LIST import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.base.BaseCanvasFragment import com.instructure.pandautils.features.assignments.list.composables.AssignmentListScreen +import com.instructure.pandautils.utils.AssignmentGradedEvent import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.collectOneOffEvents import com.instructure.pandautils.utils.color import com.instructure.pandautils.utils.withArgs import dagger.hilt.android.AndroidEntryPoint +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import javax.inject.Inject @ScreenView(SCREEN_VIEW_ASSIGNMENT_LIST) @@ -58,6 +62,16 @@ class AssignmentListFragment: BaseCanvasFragment(), Bookmarkable { override val bookmark: Bookmarker by lazy { viewModel.bookmarker } + override fun onStart() { + super.onStart() + EventBus.getDefault().register(this) + } + + override fun onStop() { + super.onStop() + EventBus.getDefault().unregister(this) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -100,6 +114,14 @@ class AssignmentListFragment: BaseCanvasFragment(), Bookmarkable { return getString(R.string.assignmentListTitle) } + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onAssignmentGraded(event: AssignmentGradedEvent) { + event.once(javaClass.simpleName) { + viewModel.handleAction(AssignmentListScreenEvent.Refresh) + } + } + companion object { fun newInstance(): AssignmentListFragment { return AssignmentListFragment() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt index 450a7ae584..18d5f05c07 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt @@ -49,6 +49,7 @@ import com.instructure.pandautils.utils.orderedCheckpoints import com.instructure.pandautils.utils.toFormattedString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -79,6 +80,8 @@ class AssignmentListViewModel @Inject constructor( private var customStatuses = listOf() + private var loadJob: Job? = null + init { getAssignments(false) } @@ -88,8 +91,9 @@ class AssignmentListViewModel @Inject constructor( } private fun getAssignments(forceRefresh: Boolean = false) { + loadJob?.cancel() if (courseId != null) { - viewModelScope.tryLaunch { + loadJob = viewModelScope.tryLaunch { val course = repository.getCourse(courseId, forceRefresh) customStatuses = repository.getCustomGradeStatuses(courseId, forceRefresh) bookmarker = Bookmarker(true, course) From f165758cf717998174cd1a74419ecb704b382d5a Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Wed, 8 Oct 2025 10:35:33 +0200 Subject: [PATCH 29/94] [MBL-19199][Parent] File Upload submissions (image/PDF) fail to download refs: MBL-19199 affects: Parent release note: Fixed downloading files in Submission details. --- .../pandautils/views/CanvasWebView.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt index 860afd674a..5bb251cb32 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt @@ -359,6 +359,21 @@ class CanvasWebView @JvmOverloads constructor( inner class CanvasWebViewClient : WebViewClient() { override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { val url = request.url.toString() + + if (isCanvadocsDownload(url)) { + val fileName = request.url.lastPathSegment + if (fileName != null) { + val extension = fileName.substringAfterLast('.', "") + val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension).orEmpty() + view.post { + canvasWebViewClientCallback?.openMediaFromWebView(mimeType, url, fileName) + view.stopLoading() + view.goBack() + } + return WebResourceResponse(null, null, null) + } + } + if (isStudioDownload(url) && mediaDownloadCallback != null) { val extensionSegment = request.url.lastPathSegment val extension = extensionSegment?.substringAfterLast('.', "") ?: "" @@ -820,6 +835,10 @@ class CanvasWebView @JvmOverloads constructor( return url.contains("instructuremedia.com/fetch/") && url.contains("disposition=download") } + private fun isCanvadocsDownload(url: String): Boolean { + return url.contains("canvadocs") && url.contains("/download/") && url.contains("single_use_token=") + } + fun containsLTI(html: String, encoding: String?): Boolean { // BaseURL is set as Referer. Referer needed for some Vimeo videos to play // Studio needs the protocol attached to the referrer, so use that if we're using Studio From f44a0d97e0a2ca6c6a1037330259f55a9f20330e Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:33:11 +0200 Subject: [PATCH 30/94] [MBL-19203][Student] Push notification for teacher comment opens Assignment Details instead of Submission Details #3293 refs: MBL-19203 affects: Student release note: Fixed an issue where submission comment push notifications wouldn't route to the submission. --- .../details/AssignmentDetailsFragment.kt | 4 +- .../details/AssignmentDetailsViewModel.kt | 20 ++++ .../details/AssignmentDetailsViewModelTest.kt | 113 ++++++++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt index 0d9d3fbd64..65abd75138 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt @@ -93,6 +93,8 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo @get:PageViewUrlParam(name = "courseId") val courseId by LongArg(key = Const.COURSE_ID, default = 0) + val submissionId by LongArg(key = Const.SUBMISSION_ID, default = -1) + private var binding: FragmentAssignmentDetailsBinding? = null private val viewModel: AssignmentDetailsViewModel by viewModels() @@ -432,7 +434,7 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo if (route.paramsHash.containsKey(RouterParams.SUBMISSION_ID)) { // Indicate that we want to route to the Submission Details page - this will give us a small backstack, allowing the user to hit back and go to Assignment Details instead // of closing the app (in the case of when the app isn't running and the user hits a push notification that takes them to Submission Details) - route.arguments.putString(Const.SUBMISSION_ID, route.paramsHash[RouterParams.SUBMISSION_ID]) + route.arguments.putLong(Const.SUBMISSION_ID, route.paramsHash[RouterParams.SUBMISSION_ID]?.toLong().orDefault()) } if (route.paramsHash.containsKey(RouterParams.COURSE_ID)) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt index 8125fd72d8..654d3c0392 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt @@ -114,6 +114,7 @@ class AssignmentDetailsViewModel @Inject constructor( private val _course = MutableLiveData(Course(id = courseId)) private val assignmentId = savedStateHandle.get(Const.ASSIGNMENT_ID).orDefault() + private val submissionId = savedStateHandle.get(Const.SUBMISSION_ID) var bookmarker = Bookmarker(true, course.value).withParam(RouterParams.ASSIGNMENT_ID, assignmentId.toString()) @@ -231,6 +232,25 @@ class AssignmentDetailsViewModel @Inject constructor( ) } _data.postValue(getViewData(assignmentResult, hasDraft)) _state.postValue(ViewState.Success) + + // Check if we need to auto-navigate to submission details from push notification + submissionId?.let { subId -> + val submission = assignmentResult.submission + if (submission != null + && submission.id == subId + && submission.submissionType != SubmissionType.NOT_GRADED.apiString + && submission.submissionType != SubmissionType.ON_PAPER.apiString) + { + postAction( + AssignmentDetailAction.NavigateToSubmissionScreen( + isObserver, + submission.attempt, + assignmentResult.htmlUrl, + isAssignmentEnhancementEnabled + ) + ) + } + } } catch (ex: Exception) { val errorString = if (ex is IllegalAccessException) { resources.getString(R.string.assignmentNoLongerAvailable) diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt index 3b30833801..ba029c2f42 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt @@ -112,6 +112,7 @@ class AssignmentDetailsViewModelTest { every { savedStateHandle.get(Const.COURSE_ID) } returns 0L every { savedStateHandle.get(Const.ASSIGNMENT_ID) } returns 0L + every { savedStateHandle.get(Const.SUBMISSION_ID) } returns 0L every { apiPrefs.user } returns User(id = 1) every { themePrefs.textButtonColor } returns 0 @@ -1008,4 +1009,116 @@ class AssignmentDetailsViewModelTest { ) } } + + @Test + fun `loadData navigates to submission screen when submissionId is provided and matches`() { + val submissionId = 12345L + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val submission = Submission(id = submissionId, attempt = 2, submissionType = "online_text_entry") + val assignment = Assignment(id = 1L, htmlUrl = "https://assignment.url", submission = submission) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment + coEvery { assignmentDetailsRepository.isAssignmentEnhancementEnabled(any(), any()) } returns true + + every { savedStateHandle.get(Const.SUBMISSION_ID) } returns submissionId + + val viewModel = getViewModel() + + val expected = AssignmentDetailAction.NavigateToSubmissionScreen( + isObserver = false, + selectedSubmissionAttempt = 2L, + assignmentUrl = "https://assignment.url", + isAssignmentEnhancementEnabled = true + ) + assertEquals(expected, viewModel.events.value?.peekContent()) + } + + @Test + fun `loadData does not navigate when submissionId is not provided`() { + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val submission = Submission(id = 12345L, attempt = 2, submissionType = "online_text_entry") + val assignment = Assignment(id = 1L, htmlUrl = "https://assignment.url", submission = submission) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment + coEvery { assignmentDetailsRepository.isAssignmentEnhancementEnabled(any(), any()) } returns true + + every { savedStateHandle.get(Const.SUBMISSION_ID) } returns null + + val viewModel = getViewModel() + + assertFalse(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToSubmissionScreen) + } + + @Test + fun `loadData does not navigate when submissionId does not match`() { + val submissionId = 99999L + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val submission = Submission(id = 12345L, attempt = 2, submissionType = "online_text_entry") + val assignment = Assignment(id = 1L, htmlUrl = "https://assignment.url", submission = submission) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment + coEvery { assignmentDetailsRepository.isAssignmentEnhancementEnabled(any(), any()) } returns true + + every { savedStateHandle.get(Const.SUBMISSION_ID) } returns submissionId + + val viewModel = getViewModel() + + assertFalse(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToSubmissionScreen) + } + + @Test + fun `loadData does not navigate when submission is null`() { + val submissionId = 12345L + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val assignment = Assignment(id = 1L, htmlUrl = "https://assignment.url", submission = null) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment + coEvery { assignmentDetailsRepository.isAssignmentEnhancementEnabled(any(), any()) } returns true + + every { savedStateHandle.get(Const.SUBMISSION_ID) } returns submissionId + + val viewModel = getViewModel() + + assertFalse(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToSubmissionScreen) + } + + @Test + fun `loadData does not navigate when submission type is not_graded`() { + val submissionId = 12345L + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val submission = Submission(id = submissionId, attempt = 2, submissionType = "not_graded") + val assignment = Assignment(id = 1L, htmlUrl = "https://assignment.url", submission = submission) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment + coEvery { assignmentDetailsRepository.isAssignmentEnhancementEnabled(any(), any()) } returns true + + every { savedStateHandle.get(Const.SUBMISSION_ID) } returns submissionId + + val viewModel = getViewModel() + + assertFalse(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToSubmissionScreen) + } + + @Test + fun `loadData does not navigate when submission type is on_paper`() { + val submissionId = 12345L + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val submission = Submission(id = submissionId, attempt = 2, submissionType = "on_paper") + val assignment = Assignment(id = 1L, htmlUrl = "https://assignment.url", submission = submission) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment + coEvery { assignmentDetailsRepository.isAssignmentEnhancementEnabled(any(), any()) } returns true + + every { savedStateHandle.get(Const.SUBMISSION_ID) } returns submissionId + + val viewModel = getViewModel() + + assertFalse(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToSubmissionScreen) + } } From a798d994f5d5cbfb1d1bfc62ef2b9f5a7ba3ef84 Mon Sep 17 00:00:00 2001 From: andrasmaczak Date: Wed, 8 Oct 2025 12:39:49 +0200 Subject: [PATCH 31/94] [CLX-3022][Horizon] Learn screen accessibility (#3290) refs: CLX-3022 affects: Horizon release note: --- .../course/card/DashboardCourseCardContent.kt | 7 ++- .../learn/course/CourseDetailsScreen.kt | 10 +++++ .../learn/course/score/CourseScoreScreen.kt | 28 +++++++++--- .../horizon/horizonui/Extensions.kt | 43 +++++++++++++++++++ .../organisms/cards/CollapsableContentCard.kt | 21 +++++++-- .../organisms/cards/ModuleContainer.kt | 9 +++- .../organisms/cards/ModuleItemCard.kt | 6 +-- .../inputs/singleselect/SingleSelect.kt | 18 +++++++- libs/horizon/src/main/res/values/strings.xml | 8 ++++ 9 files changed, 134 insertions(+), 16 deletions(-) create mode 100644 libs/horizon/src/main/java/com/instructure/horizon/horizonui/Extensions.kt diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardContent.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardContent.kt index bac0c634a1..39e90eb2a1 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardContent.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/course/card/DashboardCourseCardContent.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLinkStyles @@ -198,7 +199,11 @@ private fun ProgramsText( if (parts.size > 1) append(parts[1]) } - Text(style = HorizonTypography.p1, text = fullText) + Text( + modifier = Modifier.semantics(mergeDescendants = true) {}, + style = HorizonTypography.p1, + text = fullText + ) } @Composable diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt index ee492f4ed1..62ed59915a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt @@ -33,7 +33,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.instructure.horizon.features.learn.course.lti.CourseToolsScreen @@ -45,6 +49,7 @@ import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.molecules.ProgressBar import com.instructure.horizon.horizonui.organisms.tabrow.TabRow +import com.instructure.horizon.horizonui.selectable import kotlinx.coroutines.launch @Composable @@ -143,12 +148,17 @@ private fun Tab(tab: CourseDetailsTab, isSelected: Boolean, modifier: Modifier = modifier = modifier .padding(bottom = 2.dp) ) { + val context = LocalContext.current Text( stringResource(tab.titleRes), style = HorizonTypography.p1, color = color, modifier = Modifier .padding(top = 20.dp) + .semantics { + role = Role.Tab + selectable(context, isSelected) + } ) } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/score/CourseScoreScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/score/CourseScoreScreen.kt index 588d684925..13e0b2f4bd 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/score/CourseScoreScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/score/CourseScoreScreen.kt @@ -14,6 +14,8 @@ * along with this program. If not, see . * */ +@file:OptIn(ExperimentalComposeUiApi::class) + package com.instructure.horizon.features.learn.course.score import androidx.compose.animation.AnimatedContent @@ -41,11 +43,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -352,28 +358,36 @@ private fun GroupWeightsContent(assignmentGroups: List @Composable private fun GroupWeightItem(assignmentGroup: AssignmentGroupScoreItem) { + val groupWeightText = stringResource( + R.string.weightPercentageValue, + assignmentGroup.groupWeight + ) + val mergedContentDescription = "${assignmentGroup.name}, $groupWeightText" + Column( modifier = Modifier .padding(vertical = 16.dp) + .semantics(mergeDescendants = true) { + contentDescription = mergedContentDescription + } ) { Column ( modifier = Modifier.padding(horizontal = 24.dp), ) { Text( - text = assignmentGroup.name.orEmpty(), + text = assignmentGroup.name, style = HorizonTypography.p1, - color = HorizonColors.Text.body() + color = HorizonColors.Text.body(), + modifier = Modifier.semantics { invisibleToUser() } ) HorizonSpace(SpaceSize.SPACE_8) Text( - text = stringResource( - R.string.weightPercentageValue, - assignmentGroup.groupWeight - ), + text = groupWeightText, style = HorizonTypography.p1, - color = HorizonColors.Text.body() + color = HorizonColors.Text.body(), + modifier = Modifier.semantics { invisibleToUser() } ) } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/Extensions.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/Extensions.kt new file mode 100644 index 0000000000..dea6e9850e --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/Extensions.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 - 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.horizon.horizonui + +import android.content.Context +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.stateDescription +import com.instructure.horizon.R + +fun SemanticsPropertyReceiver.expandable(context: Context, expanded: Boolean) { + val expandedStateDesc = context.getString(R.string.a11y_expanded) + val collapsedStateDesc = context.getString(R.string.a11y_collapsed) + val expandActionLabel = context.getString(R.string.a11y_expand) + val collapseActionLabel = context.getString(R.string.a11y_collapse) + + stateDescription = if (expanded) expandedStateDesc else collapsedStateDesc + liveRegion = LiveRegionMode.Assertive + onClick(if (expanded) collapseActionLabel else expandActionLabel) { false } +} + +fun SemanticsPropertyReceiver.selectable(context: Context, selected: Boolean) { + val selectedStateDesc = context.getString(R.string.a11y_selected) + val unselectedStateDesc = context.getString(R.string.a11y_unselected) + + stateDescription = if (selected) selectedStateDesc else unselectedStateDesc + liveRegion = LiveRegionMode.Assertive +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/CollapsableContentCard.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/CollapsableContentCard.kt index 7ec7ec7c8f..5db2a4128f 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/CollapsableContentCard.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/CollapsableContentCard.kt @@ -32,19 +32,25 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.instructure.horizon.R +import com.instructure.horizon.horizonui.expandable import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius import com.instructure.horizon.horizonui.foundation.HorizonSpace import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.foundation.SpaceSize +@OptIn(ExperimentalComposeUiApi::class) @Composable fun CollapsableContentCard( title: String, @@ -65,13 +71,18 @@ fun CollapsableContentCard( .padding(vertical = 16.dp) ) { Column( - modifier = Modifier.clickable { onExpandChanged(!expanded) } + modifier = Modifier + .clickable { onExpandChanged(!expanded) } + .semantics { + invisibleToUser() + } ){ Text( title, style = HorizonTypography.h2, color = HorizonColors.Text.body(), - modifier = Modifier.padding(horizontal = 24.dp) + modifier = Modifier + .padding(horizontal = 24.dp) ) HorizonSpace(SpaceSize.SPACE_16) @@ -80,11 +91,15 @@ fun CollapsableContentCard( targetValue = if (expanded) 180f else 0f, label = "rotationAnimation" ) - + val context = LocalContext.current Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) + .semantics(mergeDescendants = true) { + expandable(context, expanded) + } + ) { Icon( painter = painterResource(R.drawable.keyboard_arrow_down), diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleContainer.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleContainer.kt index ed4da4be80..216f527a79 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleContainer.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleContainer.kt @@ -49,11 +49,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R +import com.instructure.horizon.horizonui.expandable import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius import com.instructure.horizon.horizonui.foundation.HorizonSpace @@ -98,8 +100,13 @@ fun ModuleContainer(state: ModuleHeaderState, modifier: Modifier = Modifier, con Column { val onClick = state.onClick val clickModifier = if (onClick != null) Modifier.clickable { onClick() } else Modifier + val context = LocalContext.current - Column(modifier = clickModifier.padding(16.dp)) { + Column(modifier = clickModifier + .semantics(mergeDescendants = true) { + expandable(context, state.expanded) + } + .padding(16.dp)) { ModuleHeader(state = state) if (state.subtitle != null && state.expanded) { HorizonSpace(SpaceSize.SPACE_24) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleItemCard.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleItemCard.kt index da427604fc..5bdb5e5e96 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleItemCard.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleItemCard.kt @@ -118,7 +118,7 @@ private fun RowScope.ModuleItemCardIcon(state: ModuleItemCardState, modifier: Mo HorizonSpace(SpaceSize.SPACE_8) Icon( painterResource(R.drawable.lock), - contentDescription = null, + contentDescription = stringResource(R.string.a11y_locked), tint = HorizonColors.Surface.institution(), modifier = modifier ) @@ -128,7 +128,7 @@ private fun RowScope.ModuleItemCardIcon(state: ModuleItemCardState, modifier: Mo HorizonSpace(SpaceSize.SPACE_8) Icon( painterResource(R.drawable.check_circle_full), - contentDescription = null, + contentDescription = stringResource(R.string.a11y_completed), tint = HorizonColors.Surface.institution(), modifier = modifier ) @@ -138,7 +138,7 @@ private fun RowScope.ModuleItemCardIcon(state: ModuleItemCardState, modifier: Mo HorizonSpace(SpaceSize.SPACE_8) Icon( painterResource(R.drawable.circle), - contentDescription = null, + contentDescription = stringResource(R.string.a11y_not_completed), tint = HorizonColors.LineAndBorder.lineStroke(), modifier = modifier ) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelect.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelect.kt index 043ff8284b..0eb05b0ec7 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelect.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelect.kt @@ -39,6 +39,12 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.instructure.canvasapi2.utils.ContextKeeper @@ -55,7 +61,8 @@ fun SingleSelect( state: SingleSelectState, modifier: Modifier = Modifier ) { - + val expandedState = stringResource(R.string.a11y_expanded) + val collapsedState = stringResource(R.string.a11y_collapsed) Input( label = state.label, helperText = state.helperText, @@ -65,6 +72,15 @@ fun SingleSelect( .onFocusChanged { state.onFocusChanged(it.isFocused) } + .clearAndSetSemantics { + role = Role.DropdownList + stateDescription = if (state.isMenuOpen) expandedState else collapsedState + contentDescription = if (state.selectedOption != null) { + "${state.label}, ${state.selectedOption}" + } else { + state.label ?: "" + } + } ) { Column( modifier = Modifier diff --git a/libs/horizon/src/main/res/values/strings.xml b/libs/horizon/src/main/res/values/strings.xml index d17872d8b6..16936cde6f 100644 --- a/libs/horizon/src/main/res/values/strings.xml +++ b/libs/horizon/src/main/res/values/strings.xml @@ -370,4 +370,12 @@ Congrats! You’ve completed your course. View your progress and scores on the Learn page. Select Course Close + Expanded + Collapsed + Expand + Collapse + Completed + Not completed + Locked + Unselected \ No newline at end of file From 1a1aab3f18ae3cbd4487d58b46d9114593b6a4e2 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:38:43 +0200 Subject: [PATCH 32/94] [MBL-19300][Teacher] Fix push notification routing to open correct student submission in SpeedGrader (#3292) Fixed an issue where tapping a push notification about a student comment would open SpeedGrader with the first submission instead of the corresponding student's submission. Changes: - Added submissionId parameter handling in SpeedGraderViewModel - When submissionIds are empty, fetch all submissions and calculate selectedItem based on the provided submissionId - Updated UI state to reflect the correct selected submission - Added comprehensive unit tests for the new behavior Test plan: Since push notifications only work in production, this can be tested using a deep link to open SpeedGrader with a specific submission ID. Verify that the correct student's submission is displayed (not the first one). refs: MBL-19300 affects: Teacher release note: Fixed an issue where tapping a push notification about a student comment would open SpeedGrader with the first submission instead of the corresponding student's submission. --- .../features/speedgrader/SpeedGraderScreen.kt | 9 +- .../speedgrader/SpeedGraderViewModel.kt | 19 ++- .../speedgrader/SpeedGraderViewModelTest.kt | 143 ++++++++++++++++++ 3 files changed, 162 insertions(+), 9 deletions(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderScreen.kt index 4a4d721564..1f4ef75f47 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderScreen.kt @@ -70,10 +70,6 @@ fun SpeedGraderScreen( val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - val pagerState = rememberPagerState( - pageCount = { uiState.submissionIds.size }, - initialPage = uiState.selectedItem - ) val viewPagerEnabled by sharedViewModel.viewPagerEnabled.collectAsState(initial = true) val errorEvent by sharedViewModel.errorState.collectAsState(initial = null) @@ -151,6 +147,11 @@ fun SpeedGraderScreen( } else -> { + val pagerState = rememberPagerState( + pageCount = { uiState.submissionIds.size }, + initialPage = uiState.selectedItem + ) + HorizontalPager( modifier = Modifier.padding(padding), state = pagerState, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModel.kt index 29783b9e30..af355f22a6 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModel.kt @@ -52,6 +52,8 @@ class SpeedGraderViewModel @Inject constructor( private val selectedItem: Int = savedStateHandle[Const.SELECTED_ITEM] ?: 0 + private val submissionId: Long? = savedStateHandle[Const.SUBMISSION_ID] + private var assignment: Assignment? = null private val _uiState = MutableStateFlow( @@ -90,14 +92,20 @@ class SpeedGraderViewModel @Inject constructor( courseId = courseId, forceNetwork = false ) - val ids = if (submissionIds.isEmpty()) { - assignmentSubmissionRepository.getGradeableStudentSubmissions( + val ids: List + val selectedItem: Int + if (submissionIds.isEmpty()) { + val submissions = assignmentSubmissionRepository.getGradeableStudentSubmissions( assignmentId, courseId, false - ).map { it.id } + ) + val submissionIndex = submissions.indexOfFirst { it.submission?.id == submissionId } + selectedItem = if (submissionIndex == -1) 0 else submissionIndex + ids = submissions.map { it.id } } else { - submissionIds.toList() + ids = submissionIds.toList() + selectedItem = _uiState.value.selectedItem } val assignmentDetails = repository.getAssignmentDetails(assignmentId) _uiState.update { @@ -105,7 +113,8 @@ class SpeedGraderViewModel @Inject constructor( assignmentName = assignmentDetails.assignment?.title.orEmpty(), courseName = assignmentDetails.assignment?.course?.name.orEmpty(), loading = false, - submissionIds = ids + submissionIds = ids, + selectedItem = selectedItem ) } } catch (e: Exception) { diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModelTest.kt index e1a478d1ed..4760a94701 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/SpeedGraderViewModelTest.kt @@ -198,4 +198,147 @@ class SpeedGraderViewModelTest { errorHandler.postError("Error", any()) } } + + @Test + fun `fetchData uses submissionIds when provided`() = runTest { + val course = AssignmentDetailsQuery.Course(name = "Test Course", _id = "1") + val assignment = AssignmentDetailsQuery.Assignment(title = "Test Assignment", course = course) + val assignmentDetails = AssignmentDetailsQuery.Data(assignment = assignment) + coEvery { repository.getAssignmentDetails(1L) } returns assignmentDetails + + savedStateHandle = SavedStateHandle( + mapOf( + Const.COURSE_ID to 1L, + Const.ASSIGNMENT_ID to 1L, + SpeedGraderFragment.FILTERED_SUBMISSION_IDS to longArrayOf(10L, 20L, 30L), + Const.SELECTED_ITEM to 1 + ) + ) + + createViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + val uiState = viewModel.uiState.first() + assertEquals(listOf(10L, 20L, 30L), uiState.submissionIds) + assertEquals(1, uiState.selectedItem) + coVerify(exactly = 0) { assignmentSubmissionRepository.getGradeableStudentSubmissions(any(), any(), any()) } + } + + @Test + fun `fetchData loads all submissions when submissionIds empty and sets selectedItem to 0 when submissionId not found`() = runTest { + val course = AssignmentDetailsQuery.Course(name = "Test Course", _id = "1") + val assignment = AssignmentDetailsQuery.Assignment(title = "Test Assignment", course = course) + val assignmentDetails = AssignmentDetailsQuery.Data(assignment = assignment) + coEvery { repository.getAssignmentDetails(1L) } returns assignmentDetails + + val mockSubmissions = listOf( + mockk(relaxed = true).apply { + coEvery { id } returns 100L + coEvery { submission?.id } returns 1000L + }, + mockk(relaxed = true).apply { + coEvery { id } returns 200L + coEvery { submission?.id } returns 2000L + }, + mockk(relaxed = true).apply { + coEvery { id } returns 300L + coEvery { submission?.id } returns 3000L + } + ) + coEvery { assignmentSubmissionRepository.getGradeableStudentSubmissions(1L, 1L, false) } returns mockSubmissions + + savedStateHandle = SavedStateHandle( + mapOf( + Const.COURSE_ID to 1L, + Const.ASSIGNMENT_ID to 1L, + SpeedGraderFragment.FILTERED_SUBMISSION_IDS to longArrayOf(), + Const.SUBMISSION_ID to 9999L + ) + ) + + createViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + val uiState = viewModel.uiState.first() + assertEquals(listOf(100L, 200L, 300L), uiState.submissionIds) + assertEquals(0, uiState.selectedItem) + coVerify(exactly = 1) { assignmentSubmissionRepository.getGradeableStudentSubmissions(1L, 1L, false) } + } + + @Test + fun `fetchData loads all submissions when submissionIds empty and sets selectedItem based on submissionId`() = runTest { + val course = AssignmentDetailsQuery.Course(name = "Test Course", _id = "1") + val assignment = AssignmentDetailsQuery.Assignment(title = "Test Assignment", course = course) + val assignmentDetails = AssignmentDetailsQuery.Data(assignment = assignment) + coEvery { repository.getAssignmentDetails(1L) } returns assignmentDetails + + val mockSubmissions = listOf( + mockk(relaxed = true).apply { + coEvery { id } returns 100L + coEvery { submission?.id } returns 1000L + }, + mockk(relaxed = true).apply { + coEvery { id } returns 200L + coEvery { submission?.id } returns 2000L + }, + mockk(relaxed = true).apply { + coEvery { id } returns 300L + coEvery { submission?.id } returns 3000L + } + ) + coEvery { assignmentSubmissionRepository.getGradeableStudentSubmissions(1L, 1L, false) } returns mockSubmissions + + savedStateHandle = SavedStateHandle( + mapOf( + Const.COURSE_ID to 1L, + Const.ASSIGNMENT_ID to 1L, + SpeedGraderFragment.FILTERED_SUBMISSION_IDS to longArrayOf(), + Const.SUBMISSION_ID to 2000L + ) + ) + + createViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + val uiState = viewModel.uiState.first() + assertEquals(listOf(100L, 200L, 300L), uiState.submissionIds) + assertEquals(1, uiState.selectedItem) + coVerify(exactly = 1) { assignmentSubmissionRepository.getGradeableStudentSubmissions(1L, 1L, false) } + } + + @Test + fun `fetchData loads all submissions when submissionIds empty without submissionId parameter`() = runTest { + val course = AssignmentDetailsQuery.Course(name = "Test Course", _id = "1") + val assignment = AssignmentDetailsQuery.Assignment(title = "Test Assignment", course = course) + val assignmentDetails = AssignmentDetailsQuery.Data(assignment = assignment) + coEvery { repository.getAssignmentDetails(1L) } returns assignmentDetails + + val mockSubmissions = listOf( + mockk(relaxed = true).apply { + coEvery { id } returns 100L + coEvery { submission?.id } returns 1000L + }, + mockk(relaxed = true).apply { + coEvery { id } returns 200L + coEvery { submission?.id } returns 2000L + } + ) + coEvery { assignmentSubmissionRepository.getGradeableStudentSubmissions(1L, 1L, false) } returns mockSubmissions + + savedStateHandle = SavedStateHandle( + mapOf( + Const.COURSE_ID to 1L, + Const.ASSIGNMENT_ID to 1L, + SpeedGraderFragment.FILTERED_SUBMISSION_IDS to longArrayOf() + ) + ) + + createViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + val uiState = viewModel.uiState.first() + assertEquals(listOf(100L, 200L), uiState.submissionIds) + assertEquals(0, uiState.selectedItem) + coVerify(exactly = 1) { assignmentSubmissionRepository.getGradeableStudentSubmissions(1L, 1L, false) } + } } From 553ca79a88b5c2eef352d190742cf7b14cc9e538 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:16:59 +0200 Subject: [PATCH 33/94] [MBL-19385][All] Fix light mode button disappearing after rotation (#3285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed issue where the "Switch to Light Mode" button in calendar event details would disappear after screen rotation and the theme state would not persist. refs: MBL-19385 affects: Parent, Student, Teacher release note: Fixed light mode button disappearing after screen rotation in calendar event details test plan: Enable dark mode on device Navigate to calendar event with HTML description Verify "Switch to Light Mode" button appears Rotate device - button should remain visible Toggle to light mode, rotate device - light mode should persist 🤖 Generated with Claude Code --- .../ComposeCanvasWebViewWrapper.kt | 33 ++++++-- .../pandautils/views/CanvasWebViewWrapper.kt | 80 +++++++++++++++++++ 2 files changed, 105 insertions(+), 8 deletions(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ComposeCanvasWebViewWrapper.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ComposeCanvasWebViewWrapper.kt index 700a93eaab..442a44cccd 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ComposeCanvasWebViewWrapper.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ComposeCanvasWebViewWrapper.kt @@ -21,8 +21,11 @@ import android.webkit.WebView import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.viewinterop.AndroidView import androidx.core.os.bundleOf @@ -47,6 +50,13 @@ fun ComposeCanvasWebViewWrapper( embeddedWebViewCallbacks: ComposeEmbeddedWebViewCallbacks? = null, ) { val webViewState = rememberSaveable(content) { bundleOf() } + val savedHtml = rememberSaveable(content, stateSaver = Saver( + save = { it }, + restore = { it } + )) { mutableStateOf(content) } + val savedThemeSwitched = rememberSaveable { bundleOf("themeSwitched" to false) } + val configuration = LocalConfiguration.current + val configKey = "${configuration.orientation}-${configuration.uiMode}" if (LocalInspectionMode.current) { Text(text = content) @@ -84,27 +94,34 @@ fun ComposeCanvasWebViewWrapper( applyOnWebView?.let { applyOnWebView -> webView.applyOnWebView() } } }, - update = { + update = { view -> + configKey // Read configuration to trigger update on change + savedHtml.value = content // Update saved HTML on each update if (webViewState.isEmpty) { if (useInAppFormatting) { - it.loadHtml(content, title) + view.loadHtml(savedHtml.value, title) } else { - it.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), content, contentType, "UTF-8", null) + view.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), savedHtml.value, contentType, "UTF-8", null) } if (onLtiButtonPressed != null) { - it.webView.addJavascriptInterface(JsExternalToolInterface(onLtiButtonPressed), Const.LTI_TOOL) + view.webView.addJavascriptInterface(JsExternalToolInterface(onLtiButtonPressed), Const.LTI_TOOL) } - if (HtmlContentFormatter.hasGoogleDocsUrl(content)) { - it.webView.addJavascriptInterface(JsGoogleDocsInterface(it.context), Const.GOOGLE_DOCS) + if (HtmlContentFormatter.hasGoogleDocsUrl(savedHtml.value)) { + view.webView.addJavascriptInterface(JsGoogleDocsInterface(view.context), Const.GOOGLE_DOCS) } + view.handleConfigurationChange() } else { - it.webView.restoreState(webViewState) + view.webView.restoreState(webViewState) + view.setHtmlContent(savedHtml.value) + view.setThemeSwitched(savedThemeSwitched.getBoolean("themeSwitched", false)) + view.handleConfigurationChange(reloadContent = false) } - applyOnUpdate?.let { applyOnUpdate -> it.applyOnUpdate() } + applyOnUpdate?.let { applyOnUpdate -> view.applyOnUpdate() } }, onRelease = { + savedThemeSwitched.putBoolean("themeSwitched", it.themeSwitched) it.webView.saveState(webViewState) }, modifier = modifier.fillMaxSize() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebViewWrapper.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebViewWrapper.kt index faeba543a5..0b9c1e06ab 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebViewWrapper.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebViewWrapper.kt @@ -18,10 +18,13 @@ package com.instructure.pandautils.views import android.content.Context import android.content.res.Configuration +import android.os.Parcel +import android.os.Parcelable import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout import androidx.annotation.ColorRes +import androidx.core.view.isVisible import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.R import com.instructure.pandautils.databinding.ViewCanvasWebViewWrapperBinding @@ -137,6 +140,13 @@ open class CanvasWebViewWrapper @JvmOverloads constructor( } private fun initVisibility(html: String) { + updateButtonVisibility(html) + if (binding.themeSwitchButton.visibility == android.view.View.VISIBLE) { + changeButtonTheme() + } + } + + private fun updateButtonVisibility(html: String) { val nightModeFlags: Int = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES && html.isNotEmpty()) { binding.themeSwitchButton.setVisible() @@ -145,6 +155,76 @@ open class CanvasWebViewWrapper @JvmOverloads constructor( } } + override fun onConfigurationChanged(newConfig: Configuration?) { + super.onConfigurationChanged(newConfig) + handleConfigurationChange() + } + + fun setHtmlContent(content: String?) { + this.html = content + } + + fun setThemeSwitched(switched: Boolean) { + this.themeSwitched = switched + } + + fun handleConfigurationChange(reloadContent: Boolean = true) { + html?.let { htmlContent -> + updateButtonVisibility(htmlContent) + if (binding.themeSwitchButton.isVisible) { + changeButtonTheme() + if (reloadContent) { + changeContentTheme(htmlContent) + } + } + } + } + + override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + return SavedState(superState).apply { + this.themeSwitched = this@CanvasWebViewWrapper.themeSwitched + this.html = this@CanvasWebViewWrapper.html + } + } + + override fun onRestoreInstanceState(state: Parcelable?) { + when (state) { + is SavedState -> { + super.onRestoreInstanceState(state.superState) + themeSwitched = state.themeSwitched + html = state.html + } + else -> super.onRestoreInstanceState(state) + } + } + + private class SavedState : BaseSavedState { + var themeSwitched: Boolean = false + var html: String? = null + + constructor(superState: Parcelable?) : super(superState) + + private constructor(parcel: Parcel) : super(parcel) { + themeSwitched = parcel.readInt() == 1 + html = parcel.readString() + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeInt(if (themeSwitched) 1 else 0) + out.writeString(html) + } + + companion object { + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel) = SavedState(parcel) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + } + private fun formatHtml(data: String): String { val textDarkest = colorResToHexString(if (themeSwitched) R.color.licorice else R.color.textDarkest) val textDark = colorResToHexString(if (themeSwitched) R.color.ash else R.color.textDark) From d98330d6b2e29b6350584a0d9f4a91cea3726f6f Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:41:10 +0200 Subject: [PATCH 34/94] =?UTF-8?q?[MBL-19390][Teacher]=20Letter=20Grade=20a?= =?UTF-8?q?ssignments=20can=E2=80=99t=20be=20marked=20as=20Excused=20after?= =?UTF-8?q?=20a=20grade=20is=20entered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs: MBL-19390 affects: Teacher release note: Fixed an issue that occurred when attempting to excuse a letter grade assignment. --- .../speedgrader/grade/grading/SpeedGraderGradingScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt index eb9b921d3e..02321940ba 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt @@ -456,7 +456,7 @@ private fun LetterGradeGradingTypeInput(uiState: SpeedGraderGradingUiState) { } LaunchedEffect(selectedGrade) { - if (selectedGrade != uiState.enteredGrade) { + if (selectedGrade != uiState.enteredGrade && uiState.letterGrades.any { it.name == selectedGrade }) { uiState.onPercentageChange( uiState.letterGrades .find { it.name == selectedGrade } From 73ff3fa549ddf1b365bc878ff72133c1fdd074c1 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:36:49 +0200 Subject: [PATCH 35/94] [MBL-19392][Teacher]The comment box does not display above the keyboard completely #3296 refs: MBL-19392 affects: Teacher release note: Fixed an issue where the keyboard would hide the send button in SpeedGrader comments. --- .../composables/SpeedGraderCommentInput.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/composables/SpeedGraderCommentInput.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/composables/SpeedGraderCommentInput.kt index 9e3381455f..ce627416eb 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/composables/SpeedGraderCommentInput.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/composables/SpeedGraderCommentInput.kt @@ -25,6 +25,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -33,7 +35,13 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource @@ -44,8 +52,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.instructure.pandautils.R import com.instructure.pandautils.compose.LocalCourseColor +import kotlinx.coroutines.delay +@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable fun SpeedGraderCommentInput( commentText: String, @@ -57,10 +67,21 @@ fun SpeedGraderCommentInput( isOnCommentLibrary: Boolean = false, testTag: String = "speedGraderCommentInputField" ) { + val bringIntoViewRequester = remember { BringIntoViewRequester() } + var isFocused by remember { mutableStateOf(false) } + + LaunchedEffect(isFocused, commentText) { + if (isFocused) { + delay(300) // Wait for keyboard animation + bringIntoViewRequester.bringIntoView() + } + } + Column( modifier = modifier .fillMaxWidth() .wrapContentHeight() + .bringIntoViewRequester(bringIntoViewRequester) .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp) .border( width = 1.dp, @@ -73,6 +94,9 @@ fun SpeedGraderCommentInput( .fillMaxWidth() .wrapContentHeight() .background(Color.Transparent) + .onFocusChanged { focusState -> + isFocused = focusState.isFocused + } .testTag(testTag), label = { Text(text = stringResource(R.string.speedGraderCommentHint)) }, value = commentText, From ec04e78bc3f438263402d0f98ccb6216bb16fb87 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Mon, 13 Oct 2025 08:58:33 +0200 Subject: [PATCH 36/94] [MBL-19273][Teacher] Text submission's text size is changing automatically after change orientation from portrait to landscape #3298 refs: MBL-19273 affects: Teacher release note: Fixed an issue where the text size would change on orientation change in text submissions. --- .../fragments/SpeedGraderTextSubmissionFragment.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt index b1ef9a5910..bcda8edf03 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt @@ -22,10 +22,10 @@ import android.view.View import android.view.ViewGroup import android.webkit.WebChromeClient import android.webkit.WebView -import com.instructure.pandautils.base.BaseCanvasFragment import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.analytics.SCREEN_VIEW_SPEED_GRADER_TEXT_SUBMISSION import com.instructure.pandautils.analytics.ScreenView +import com.instructure.pandautils.base.BaseCanvasFragment import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.StringArg import com.instructure.pandautils.utils.setGone @@ -77,10 +77,14 @@ class SpeedGraderTextSubmissionFragment : BaseCanvasFragment(), SpeedGraderWebNa } textSubmissionWebViewWrapper.webView.canvasEmbeddedWebViewCallback = object : CanvasWebView.CanvasEmbeddedWebViewCallback { - override fun launchInternalWebViewFragment(url: String) = requireActivity().startActivity(InternalWebViewActivity.createIntent(requireActivity(), url, "", true)) + override fun launchInternalWebViewFragment(url: String) = + requireActivity().startActivity(InternalWebViewActivity.createIntent(requireActivity(), url, "", true)) + override fun shouldLaunchInternalWebViewFragment(url: String): Boolean = true } + textSubmissionWebViewWrapper.webView.setInitialScale(100) + textSubmissionWebViewWrapper.loadHtml(submissionText, getString(R.string.a11y_submissionText)) } From c7f5da05ae09d958a3a860a8283cce530b167b6b Mon Sep 17 00:00:00 2001 From: andrasmaczak Date: Mon, 13 Oct 2025 11:17:27 +0200 Subject: [PATCH 37/94] [MBL-19126][Student][Parent] Assignment Details DCP refs: MBL-19126 affects: Student, Parent release note: Details page of assignments with discussion checkpoints now shown with proper data. --- .../src/main/java/GlobalDependencies.kt | 2 +- .../e2e/compose/AssignmentReminderE2ETest.kt | 2 +- .../AssignmentDetailsInteractionTest.kt | 70 +- .../ParentCalendarInteractionTest.kt | 2 +- .../parentapp/utils/ParentComposeTest.kt | 3 + .../instructure/parentapp/utils/ParentTest.kt | 3 - .../student/ui/e2e/classic/FilesE2ETest.kt | 11 +- .../student/ui/e2e/classic/GradesE2ETest.kt | 4 +- .../student/ui/e2e/classic/ModulesE2ETest.kt | 4 +- .../ui/e2e/classic/ShareExtensionE2ETest.kt | 14 +- .../e2e/classic/k5/ImportantDatesE2ETest.kt | 4 +- .../ui/e2e/classic/k5/ScheduleE2ETest.kt | 4 +- .../classic/offline/OfflineGradesE2ETest.kt | 4 +- .../classic/offline/OfflineModulesE2ETest.kt | 4 +- .../AssignmentDetailsInteractionTest.kt | 72 +- .../ui/interaction/ModuleInteractionTest.kt | 4 +- .../NotificationInteractionTest.kt | 4 +- .../PickerSubmissionUploadInteractionTest.kt | 11 +- .../ui/interaction/ScheduleInteractionTest.kt | 4 +- .../StudentCalendarInteractionTest.kt | 2 +- .../SubmissionDetailsInteractionTest.kt | 12 +- .../ui/interaction/TodoInteractionTest.kt | 4 +- .../classic/StudentAssignmentDetailsPage.kt | 3 +- .../student/ui/utils/StudentComposeTest.kt | 10 + .../student/ui/utils/StudentTest.kt | 2 - .../interaction/GradesInteractionTest.kt | 2 +- .../interaction/SmartSearchInteractionTest.kt | 2 +- .../common/pages/AssignmentDetailsPage.kt | 15 +- .../canvas/espresso/mockcanvas/MockCanvas.kt | 50 +- .../endpoints/AssignmentEndpoints.kt | 3 +- .../canvasapi2/apis/AssignmentAPI.kt | 4 +- libs/pandares/src/main/res/values/strings.xml | 2 + .../13.json | 722 ++ .../6.json | 5832 +++++++++++++++++ .../room/offline/daos/CheckpointDaoTest.kt | 231 + .../daos/SubAssignmentSubmissionDaoTest.kt | 342 + .../pandautils/di/OfflineModule.kt | 20 +- .../details/AssignmentDetailsFragment.kt | 25 +- .../details/AssignmentDetailsViewData.kt | 3 +- .../details/AssignmentDetailsViewModel.kt | 102 +- .../composables/DueDateReminderLayout.kt | 82 + .../features/reminder/ReminderManager.kt | 18 +- .../features/reminder/ReminderRepository.kt | 6 +- .../features/reminder/ReminderViewState.kt | 2 + .../reminder/composables/ReminderView.kt | 11 +- .../room/appdatabase/AppDatabase.kt | 2 +- .../room/appdatabase/AppDatabaseMigrations.kt | 6 +- .../appdatabase/entities/ReminderEntity.kt | 3 +- .../room/offline/OfflineDatabase.kt | 14 +- .../room/offline/OfflineDatabaseMigrations.kt | 36 + .../room/offline/daos/CheckpointDao.kt | 40 + .../daos/SubAssignmentSubmissionDao.kt | 40 + .../room/offline/entities/AssignmentEntity.kt | 6 +- .../room/offline/entities/CheckpointEntity.kt | 68 + .../entities/DiscussionTopicHeaderEntity.kt | 7 +- .../entities/SubAssignmentSubmissionEntity.kt | 84 + .../room/offline/entities/SubmissionEntity.kt | 14 +- .../room/offline/facade/AssignmentFacade.kt | 9 +- .../room/offline/facade/SubmissionFacade.kt | 11 +- .../layout/fragment_assignment_details.xml | 48 +- .../details/AssignmentDetailsViewModelTest.kt | 192 +- .../offline/facade/AssignmentFacadeTest.kt | 13 + .../offline/facade/SubmissionFacadeTest.kt | 10 +- 63 files changed, 8124 insertions(+), 212 deletions(-) create mode 100644 libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json create mode 100644 libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json create mode 100644 libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt create mode 100644 libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubAssignmentSubmissionEntity.kt diff --git a/apps/buildSrc/src/main/java/GlobalDependencies.kt b/apps/buildSrc/src/main/java/GlobalDependencies.kt index 7120f7076b..a10251eb0d 100644 --- a/apps/buildSrc/src/main/java/GlobalDependencies.kt +++ b/apps/buildSrc/src/main/java/GlobalDependencies.kt @@ -45,6 +45,7 @@ object Versions { const val ENCRYPTED_SHARED_PREFERENCES = "1.0.0" const val JAVA_JWT = "4.5.0" const val GLANCE = "1.1.1" + const val LIVEDATA = "1.9.0" } object Libs { @@ -131,7 +132,6 @@ object Libs { const val LIFECYCLE_COMPILER = "androidx.lifecycle:lifecycle-compiler:${Versions.LIFECYCLE}" const val COMPOSE_VIEW_MODEL = "androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.LIFECYCLE}" const val COMPOSE_NAVIGATION = "androidx.navigation:navigation-compose:2.8.9" - /* Media and content handling */ const val PSPDFKIT = "com.pspdfkit:pspdfkit:${Versions.PSPDFKIT}" const val MEDIA3 = "androidx.media3:media3-exoplayer:${Versions.MEDIA3}" diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt index c065eb0492..9a62c37a2e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt @@ -141,7 +141,7 @@ class AssignmentReminderE2ETest: ParentComposeTest() { Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") assignmentReminderPage.clickAddReminder() - val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) } + val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) }.apply { add(Calendar.HOUR, -1) } Log.d(STEP_TAG, "Select '1 Day Before'.") assignmentReminderPage.clickCustomReminderOption() assignmentReminderPage.selectDate(reminderDateOneDay) diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt index e879991018..12c9a02bf9 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -40,6 +40,7 @@ import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.AlertType import com.instructure.canvasapi2.models.AlertWorkflowState import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.utils.toFormattedString @@ -104,7 +105,7 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { fun testDisplayDueDate() { val data = setupData() val calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } - val expectedDueDate = "January 31, 2023 11:59 PM" + val expectedDueDate = "Jan 31, 2023 11:59 PM" val course = data.courses.values.first() val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString()) @@ -113,6 +114,41 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { assignmentDetailsPage.assertDisplaysDate(expectedDueDate) } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testDisplayDueDates() { + val data = setupData() + var calendar = Calendar.getInstance().apply { set(2023, 0, 29, 23, 59, 0) } + val expectedReplyToTopicDueDate = "Jan 29, 2023 11:59 PM" + val replyToTopicDueDate = calendar.time.toApiString() + + calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } + val expectedReplyToEntryDueDate = "Jan 31, 2023 11:59 PM" + val replyToEntryDueDate = calendar.time.toApiString() + val course = data.courses.values.first() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = replyToTopicDueDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = replyToEntryDueDate, + pointsPossible = 10.0 + ) + ) + val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString(), checkpoints = checkpoints) + + gotoAssignment(data, assignmentWithNoDueDate) + + assignmentDetailsPage.assertDisplaysDate(expectedReplyToTopicDueDate, 0) + assignmentDetailsPage.assertDisplaysDate(expectedReplyToEntryDueDate, 1) + } + @Test fun testNavigating_viewAssignmentDetails() { // Test clicking on the Assignment item in the Assignment List to load the Assignment Details Page @@ -272,6 +308,38 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { assignmentReminderPage.assertReminderSectionDisplayed() } + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testReminderSectionsAreVisibleWhenThereAreNoFutureDueDates() { + val data = setupData() + val course = data.courses.values.first() + + val pastDate = Calendar.getInstance().apply { + add(Calendar.DAY_OF_MONTH, -1) + }.time.toApiString() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = pastDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = pastDate, + pointsPossible = 10.0 + ) + ) + val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = pastDate, checkpoints = checkpoints) + + gotoAssignment(data, assignment) + + assignmentDetailsPage.assertReminderViewDisplayed(0) + assignmentDetailsPage.assertReminderViewDisplayed(1) + } + @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) fun testReminderSectionIsVisibleWhenThereIsNoDueDate() { 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 index 1e00fb8342..ec849a83f3 100644 --- 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 @@ -51,7 +51,7 @@ class ParentCalendarInteractionTest : CalendarInteractionTest() { override val activityRule = ParentActivityTestRule(LoginActivity::class.java) private val dashboardPage = DashboardPage() - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) override fun goToCalendar(data: MockCanvas) { val parent = data.parents.first() 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 e5340473d0..b46c703105 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 @@ -18,6 +18,7 @@ package com.instructure.parentapp.utils import androidx.compose.ui.test.junit4.createAndroidComposeRule +import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage import com.instructure.canvas.espresso.common.pages.AssignmentReminderPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventCreateEditPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage @@ -31,6 +32,7 @@ import com.instructure.canvas.espresso.common.pages.compose.InboxDetailsPage import com.instructure.canvas.espresso.common.pages.compose.InboxSignatureSettingsPage import com.instructure.canvas.espresso.common.pages.compose.RecipientPickerPage import com.instructure.canvas.espresso.common.pages.compose.SettingsPage +import com.instructure.espresso.ModuleItemInteractions import com.instructure.parentapp.features.login.LoginActivity import com.instructure.parentapp.ui.pages.compose.AddStudentBottomPage import com.instructure.parentapp.ui.pages.compose.AlertsPage @@ -80,6 +82,7 @@ abstract class ParentComposeTest : ParentTest() { protected val calendarFilterPage = CalendarFilterPage(composeTestRule) protected val assignmentReminderPage = AssignmentReminderPage(composeTestRule) protected val inboxSignatureSettingsPage = InboxSignatureSettingsPage(composeTestRule) + protected val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), 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 69fadbaef3..d8a7a3e533 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 @@ -19,14 +19,12 @@ package com.instructure.parentapp.utils import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvas.espresso.common.pages.AboutPage -import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage import com.instructure.canvas.espresso.common.pages.CanvasNetworkSignInPage import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.canvas.espresso.common.pages.LegalPage import com.instructure.canvas.espresso.common.pages.LoginFindSchoolPage import com.instructure.canvas.espresso.common.pages.LoginLandingPage import com.instructure.canvas.espresso.common.pages.LoginSignInPage -import com.instructure.espresso.ModuleItemInteractions import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity import com.instructure.parentapp.ui.pages.classic.DashboardPage @@ -46,7 +44,6 @@ abstract class ParentTest : CanvasTest() { val dashboardPage = DashboardPage() val leftSideNavigationDrawerPage = LeftSideNavigationDrawerPage() val helpPage = HelpPage() - val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) val syllabusPage = SyllabusPage() val frontPagePage = FrontPagePage() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt index b0e5045ed2..03fd6afebe 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt @@ -18,7 +18,6 @@ package com.instructure.student.ui.e2e.classic import android.os.Environment import android.util.Log -import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents import androidx.test.platform.app.InstrumentationRegistry @@ -44,28 +43,22 @@ import com.instructure.dataseeding.util.Randomizer import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import com.instructure.student.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Rule import org.junit.Test import java.io.File import java.io.FileWriter @HiltAndroidTest -class FilesE2ETest: StudentTest() { +class FilesE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit override fun enableAndConfigureAccessibilityChecks() = Unit - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.FILES, TestCategory.E2E) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt index aec21f7875..2669a7eb0e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt @@ -21,14 +21,14 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.getDateInCanvasCalendarFormat import com.instructure.student.R -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class GradesE2ETest: StudentTest() { +class GradesE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt index 43e1ce6830..d7e6ea4b2f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt @@ -35,14 +35,14 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class ModulesE2ETest: StudentTest() { +class ModulesE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt index 0f78466c0b..cb94725be3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt @@ -19,13 +19,12 @@ package com.instructure.student.ui.e2e.classic import android.content.Intent import android.net.Uri import android.util.Log -import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.Espresso import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import com.instructure.canvas.espresso.annotations.E2E -import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType @@ -33,25 +32,20 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Rule import org.junit.Test @HiltAndroidTest -class ShareExtensionE2ETest: StudentTest() { +class ShareExtensionE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit override fun enableAndConfigureAccessibilityChecks() = Unit - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - + @Stub @E2E @Test fun shareExtensionE2ETest() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt index e4b3e95757..685cfb37fd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt @@ -31,7 +31,7 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedDataForK5 import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest @@ -41,7 +41,7 @@ import java.util.Date import java.util.Locale @HiltAndroidTest -class ImportantDatesE2ETest : StudentTest() { +class ImportantDatesE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt index 18d84e3242..54f76a8ea3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt @@ -33,7 +33,7 @@ import com.instructure.espresso.page.getStringFromResource import com.instructure.espresso.page.withAncestor import com.instructure.student.R import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedDataForK5 import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest @@ -46,7 +46,7 @@ import java.util.Locale import java.util.TimeZone @HiltAndroidTest -class ScheduleE2ETest : StudentTest() { +class ScheduleE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt index a0aae2496b..8e6e10ff9f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt @@ -37,7 +37,7 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.getDateInCanvasCalendarFormat -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import com.instructure.student.ui.utils.offline.OfflineTestUtils @@ -46,7 +46,7 @@ import org.junit.After import org.junit.Test @HiltAndroidTest -class OfflineGradesE2ETest : StudentTest() { +class OfflineGradesE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt index 49cd03c2a7..00754ec014 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt @@ -36,7 +36,7 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.seedData import com.instructure.student.ui.utils.extensions.tokenLogin import com.instructure.student.ui.utils.offline.OfflineTestUtils @@ -47,7 +47,7 @@ import org.junit.After import org.junit.Test @HiltAndroidTest -class OfflineModulesE2ETest : StudentTest() { +class OfflineModulesE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt index 44c8737c32..1575acf651 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -34,6 +34,7 @@ import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.model.SubmissionType @@ -141,7 +142,7 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { val data = setUpData() goToAssignmentList() val calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } - val expectedDueDate = "January 31, 2023 11:59 PM" + val expectedDueDate = "Jan 31, 2023 11:59 PM" val course = data.courses.values.first() val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString()) assignmentListPage.refreshAssignmentList() @@ -150,6 +151,43 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentDetailsPage.assertDisplaysDate(expectedDueDate) } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testDisplayDueDates() { + val data = setUpData() + goToAssignmentList() + + var calendar = Calendar.getInstance().apply { set(2023, 0, 29, 23, 59, 0) } + val expectedReplyToTopicDueDate = "Jan 29, 2023 11:59 PM" + val replyToTopicDueDate = calendar.time.toApiString() + + calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } + val expectedReplyToEntryDueDate = "Jan 31, 2023 11:59 PM" + val replyToEntryDueDate = calendar.time.toApiString() + val course = data.courses.values.first() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = replyToTopicDueDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = replyToEntryDueDate, + pointsPossible = 10.0 + ) + ) + val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString(), checkpoints = checkpoints) + assignmentListPage.refreshAssignmentList() + assignmentListPage.clickAssignment(assignmentWithNoDueDate) + + assignmentDetailsPage.assertDisplaysDate(expectedReplyToTopicDueDate, 0) + assignmentDetailsPage.assertDisplaysDate(expectedReplyToEntryDueDate, 1) + } + @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) fun testNavigating_viewAssignmentDetails() { @@ -403,6 +441,38 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentReminderPage.assertReminderSectionDisplayed() } + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testReminderSectionsAreVisibleWhenThereAreNoFutureDueDates() { + val data = setUpData() + val course = data.courses.values.first() + + val pastDate = Calendar.getInstance().apply { + add(Calendar.DAY_OF_MONTH, -1) + }.time.toApiString() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = pastDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = pastDate, + pointsPossible = 10.0 + ) + ) + val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = pastDate, checkpoints = checkpoints) + goToAssignmentList() + + assignmentListPage.clickAssignment(assignment) + assignmentDetailsPage.assertReminderViewDisplayed(0) + assignmentDetailsPage.assertReminderViewDisplayed(1) + } + @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) fun testReminderSectionIsVisibleWhenThereIsNoDueDate() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt index fd27493201..00b7bece2d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt @@ -57,7 +57,7 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.R import com.instructure.student.ui.pages.classic.WebViewTextCheck -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest @@ -68,7 +68,7 @@ import java.net.URLEncoder @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class ModuleInteractionTest : StudentTest() { +class ModuleInteractionTest : StudentComposeTest() { @BindValue @JvmField diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt index 6ec94e8480..f6517ba42c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt @@ -32,7 +32,7 @@ import com.instructure.canvasapi2.models.CourseSettings import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest @@ -42,7 +42,7 @@ import java.util.UUID @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class NotificationInteractionTest : StudentTest() { +class NotificationInteractionTest : StudentComposeTest() { @BindValue @JvmField diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt index 419e0b450a..86ac820578 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt @@ -22,7 +22,6 @@ import android.content.Intent import android.net.Uri import android.provider.MediaStore import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intending @@ -40,7 +39,6 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.Stub -import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvas.espresso.mockcanvas.addAssignment import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager @@ -49,7 +47,7 @@ import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment import com.instructure.pandautils.utils.FilePrefs -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest @@ -64,7 +62,7 @@ import java.io.File @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class PickerSubmissionUploadInteractionTest : StudentTest() { +class PickerSubmissionUploadInteractionTest : StudentComposeTest() { @BindValue @JvmField @@ -76,11 +74,6 @@ class PickerSubmissionUploadInteractionTest : StudentTest() { private lateinit var activity : Activity private lateinit var activityResult: Instrumentation.ActivityResult - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - @Before fun setUp() { // Read this at set-up, because it may become null soon thereafter diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt index 2f8d1f77f4..bcddb0dca6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt @@ -35,7 +35,7 @@ import com.instructure.espresso.page.getStringFromResource import com.instructure.pandautils.utils.date.DateTimeProvider import com.instructure.student.R import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.di.FakeDateTimeProvider import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.BindValue @@ -48,7 +48,7 @@ import javax.inject.Inject @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class ScheduleInteractionTest : StudentTest() { +class ScheduleInteractionTest : StudentComposeTest() { @BindValue @JvmField diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt index c95950398d..bbbea2b0ff 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt @@ -48,7 +48,7 @@ class StudentCalendarInteractionTest : CalendarInteractionTest() { override val activityRule = StudentActivityTestRule(LoginActivity::class.java) private val dashboardPage = DashboardPage() - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) private val discussionDetailsPage = DiscussionDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) override fun goToCalendar(data: MockCanvas) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt index c20f50172a..e06b5fa056 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt @@ -17,7 +17,6 @@ package com.instructure.student.ui.interaction import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.webdriver.Locator import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils @@ -27,7 +26,6 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.Stub -import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvas.espresso.mockcanvas.addAssignment import com.instructure.canvas.espresso.mockcanvas.addFileToCourse @@ -46,19 +44,18 @@ import com.instructure.canvasapi2.models.RubricCriterionRating import com.instructure.canvasapi2.models.SubmissionComment import com.instructure.espresso.handleWorkManagerTask import com.instructure.student.ui.pages.classic.WebViewTextCheck -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import org.hamcrest.Matchers -import org.junit.Rule import org.junit.Test import java.util.Date @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class SubmissionDetailsInteractionTest : StudentTest() { +class SubmissionDetailsInteractionTest : StudentComposeTest() { @BindValue @JvmField @@ -68,11 +65,6 @@ class SubmissionDetailsInteractionTest : StudentTest() { private lateinit var course: Course - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - // Should be able to add a comment on a submission @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt index 2e6b05b79d..914f24e310 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt @@ -39,7 +39,7 @@ import com.instructure.canvasapi2.models.Quiz import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest @@ -49,7 +49,7 @@ import org.junit.Test @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class TodoInteractionTest : StudentTest() { +class TodoInteractionTest : StudentComposeTest() { @BindValue @JvmField diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt index 8622e5c1f5..a0019b2354 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt @@ -16,6 +16,7 @@ package com.instructure.student.ui.pages.classic import androidx.appcompat.widget.AppCompatButton +import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import com.instructure.canvas.espresso.CanvasTest @@ -36,7 +37,7 @@ import com.instructure.espresso.typeText import com.instructure.student.R import org.hamcrest.Matchers.allOf -class StudentAssignmentDetailsPage(moduleItemInteractions: ModuleItemInteractions): AssignmentDetailsPage(moduleItemInteractions) { +class StudentAssignmentDetailsPage(moduleItemInteractions: ModuleItemInteractions, composeTestRule: ComposeTestRule): AssignmentDetailsPage(moduleItemInteractions, composeTestRule) { fun addBookmark(bookmarkName: String) { openOverflowMenu() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt index 3c3b86c9a6..5ccf520462 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt @@ -35,7 +35,10 @@ import com.instructure.canvas.espresso.common.pages.compose.SelectContextPage import com.instructure.canvas.espresso.common.pages.compose.SettingsPage import com.instructure.canvas.espresso.common.pages.compose.SmartSearchPage import com.instructure.canvas.espresso.common.pages.compose.SmartSearchPreferencesPage +import com.instructure.espresso.ModuleItemInteractions +import com.instructure.student.R import com.instructure.student.activity.LoginActivity +import com.instructure.student.ui.pages.classic.StudentAssignmentDetailsPage import org.junit.Rule abstract class StudentComposeTest : StudentTest() { @@ -60,4 +63,11 @@ abstract class StudentComposeTest : StudentTest() { val smartSearchPreferencesPage = SmartSearchPreferencesPage(composeTestRule) val assignmentListPage = AssignmentListPage(composeTestRule) val inboxSignatureSettingsPage = InboxSignatureSettingsPage(composeTestRule) + val assignmentDetailsPage = StudentAssignmentDetailsPage( + ModuleItemInteractions( + R.id.moduleName, + R.id.next_item, + R.id.prev_item + ), composeTestRule + ) } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index ecad0fc8e0..5e43e6a33c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -85,7 +85,6 @@ import com.instructure.student.ui.pages.classic.QuizTakingPage import com.instructure.student.ui.pages.classic.RemoteConfigSettingsPage import com.instructure.student.ui.pages.classic.ShareExtensionStatusPage import com.instructure.student.ui.pages.classic.ShareExtensionTargetPage -import com.instructure.student.ui.pages.classic.StudentAssignmentDetailsPage import com.instructure.student.ui.pages.classic.SubmissionDetailsPage import com.instructure.student.ui.pages.classic.SyllabusPage import com.instructure.student.ui.pages.classic.TextSubmissionUploadPage @@ -120,7 +119,6 @@ abstract class StudentTest : CanvasTest() { */ val annotationCommentListPage = AnnotationCommentListPage() val announcementListPage = AnnouncementListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn)) - val assignmentDetailsPage = StudentAssignmentDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) val bookmarkPage = BookmarkPage() val canvasWebViewPage = CanvasWebViewPage() val courseBrowserPage = CourseBrowserPage() diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt index 0412d2214e..9580c803ff 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt @@ -34,7 +34,7 @@ import org.junit.Test abstract class GradesInteractionTest : CanvasComposeTest() { private val gradesPage = GradesPage(composeTestRule) - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) @Test fun groupHeaderCollapsesAndExpandsOnClick() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt index e9e68a81c4..76cc1a1879 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt @@ -35,7 +35,7 @@ abstract class SmartSearchInteractionTest : CanvasComposeTest() { private val smartSearchPage = SmartSearchPage(composeTestRule) private val smartSearchPreferencesPage = SmartSearchPreferencesPage(composeTestRule) - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) @Test fun assertQuery() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt index 427092227c..454f337717 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt @@ -18,6 +18,10 @@ package com.instructure.canvas.espresso.common.pages import android.view.View import android.widget.ScrollView +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onData @@ -68,10 +72,9 @@ import org.hamcrest.Matchers.anyOf import org.hamcrest.Matchers.anything import org.hamcrest.Matchers.not -open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage(R.id.assignmentDetailsPage) { +open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteractions, private val composeTestRule: ComposeTestRule) : BasePage(R.id.assignmentDetailsPage) { val toolbar by OnViewWithId(R.id.toolbar) val points by OnViewWithId(R.id.points) - val date by OnViewWithId(R.id.dueDateTextView) val submissionTypes by OnViewWithId(R.id.submissionTypesTextView) fun assertDisplayToolbarTitle() { @@ -86,8 +89,8 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti onView(allOf(withText(courseNameText), withParent(R.id.toolbar))).assertDisplayed() } - fun assertDisplaysDate(dateText: String) { - date.assertHasText(dateText) + fun assertDisplaysDate(dateText: String, position: Int = 0) { + composeTestRule.onNodeWithTag("dueDateText-$position").assertTextEquals(dateText).isDisplayed() } fun assertAssignmentDetails(assignment: Assignment) { @@ -254,8 +257,8 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti onView(anyOf(withText(submissionType) + withAncestor(R.id.customPanel), withId(R.id.submissionTypesTextView) + withText(submissionType))).assertDisplayed() } - fun assertReminderViewDisplayed() { - onView(withId(R.id.reminderComposeView)).assertDisplayed() + fun assertReminderViewDisplayed(position: Int = 0) { + composeTestRule.onNodeWithTag("reminderView-$position").assertExists() } fun assertNoDescriptionViewDisplayed() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt index 93eec07573..58ddd511fc 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt @@ -40,6 +40,7 @@ import com.instructure.canvasapi2.models.CanvasColor import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.CanvasTheme +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings @@ -1106,29 +1107,40 @@ fun MockCanvas.addAssignment( withDescription: Boolean = false, gradingType: String = "percent", discussionTopicHeader: DiscussionTopicHeader? = null, - htmlUrl: String? = "" + htmlUrl: String? = "", + submission: Submission? = null, + checkpoints: List = emptyList() ) : Assignment { val assignmentId = newItemId() val submissionTypeListRawStrings = submissionTypeList.map { it.apiString } var assignment = Assignment( - id = assignmentId, - assignmentGroupId = assignmentGroupId, - courseId = courseId, - name = name, - submissionTypesRaw = submissionTypeListRawStrings, - lockInfo = lockInfo, - lockedForUser = lockInfo != null, - userSubmitted = userSubmitted, - dueAt = dueAt, - pointsPossible = pointsPossible.toDouble(), - description = description, - lockAt = lockAt, - unlockAt = unlockAt, - published = true, - allDates = listOf(AssignmentDueDate(id = newItemId(), dueAt = dueAt, lockAt = lockAt, unlockAt = unlockAt)), - gradingType = gradingType, - discussionTopicHeader = discussionTopicHeader, - htmlUrl = htmlUrl + id = assignmentId, + assignmentGroupId = assignmentGroupId, + courseId = courseId, + name = name, + submissionTypesRaw = submissionTypeListRawStrings, + lockInfo = lockInfo, + lockedForUser = lockInfo != null, + userSubmitted = userSubmitted, + dueAt = dueAt, + pointsPossible = pointsPossible.toDouble(), + description = description, + lockAt = lockAt, + unlockAt = unlockAt, + published = true, + allDates = listOf( + AssignmentDueDate( + id = newItemId(), + dueAt = dueAt, + lockAt = lockAt, + unlockAt = unlockAt + ) + ), + gradingType = gradingType, + discussionTopicHeader = discussionTopicHeader, + htmlUrl = htmlUrl, + submission = submission, + checkpoints = checkpoints ) if (isQuizzesNext) { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt index 1aa91e039c..6b9a62e654 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt @@ -218,5 +218,6 @@ private fun Assignment.toObserveeAssignment() = ObserveeAssignment( moderatedGrading = moderatedGrading, anonymousGrading = anonymousGrading, allowedAttempts = allowedAttempts, - isStudioEnabled = isStudioEnabled + isStudioEnabled = isStudioEnabled, + checkpoints = checkpoints, ) \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt index 12237e9380..87a95648c9 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt @@ -67,7 +67,7 @@ object AssignmentAPI { @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics&include[]=submission_history") fun getAssignmentWithHistory(@Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long): Call - @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics&include[]=submission_history") + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics&include[]=submission_history&include[]=checkpoints&include[]=discussion_topic&include[]=sub_assignment_submissions") suspend fun getAssignmentWithHistory( @Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long, @@ -77,7 +77,7 @@ object AssignmentAPI { @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics&include[]=submission_history") fun getAssignmentIncludeObservees(@Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long): Call - @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics&include[]=submission_history") + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics&include[]=submission_history&include[]=checkpoints&include[]=discussion_topic&include[]=sub_assignment_submissions") suspend fun getAssignmentIncludeObservees( @Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long, diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 68e112c8fb..8ccbe57bc1 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -2151,6 +2151,8 @@ Write days late Reply to topic Additional replies (%d) + Reply to topic due + Additional replies (%d) due Discussion Checkpoints Multiple Due Dates Course concluded. Unable to send messages! diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json new file mode 100644 index 0000000000..37a4c85508 --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json @@ -0,0 +1,722 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "e8a50c8d4caed97be61826c69921684e", + "entities": [ + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EnvironmentFeatureFlags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `featureFlags` TEXT NOT NULL, PRIMARY KEY(`userId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "featureFlags", + "columnName": "featureFlags", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileUploadInputEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `courseId` INTEGER, `assignmentId` INTEGER, `quizId` INTEGER, `quizQuestionId` INTEGER, `position` INTEGER, `parentFolderId` INTEGER, `action` TEXT NOT NULL, `userId` INTEGER, `attachments` TEXT NOT NULL, `submissionId` INTEGER, `filePaths` TEXT NOT NULL, `attemptId` INTEGER, `notificationId` INTEGER, PRIMARY KEY(`workerId`))", + "fields": [ + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quizQuestionId", + "columnName": "quizQuestionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filePaths", + "columnName": "filePaths", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "workerId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`))", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "PendingSubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `pageId` TEXT NOT NULL, `comment` TEXT, `date` INTEGER NOT NULL, `status` TEXT NOT NULL, `workerId` TEXT, `filePath` TEXT, `attemptId` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "filePath", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DashboardFileUploadEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `userId` INTEGER NOT NULL, `title` TEXT, `subtitle` TEXT, `courseId` INTEGER, `assignmentId` INTEGER, `attemptId` INTEGER, `folderId` INTEGER, PRIMARY KEY(`workerId`))", + "fields": [ + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subtitle", + "columnName": "subtitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "workerId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReminderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `name` TEXT NOT NULL, `text` TEXT NOT NULL, `time` INTEGER NOT NULL, `tag` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ModuleBulkProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`progressId` INTEGER NOT NULL, `allModules` INTEGER NOT NULL, `skipContentTags` INTEGER NOT NULL, `action` TEXT NOT NULL, `courseId` INTEGER NOT NULL, `affectedIds` TEXT NOT NULL, PRIMARY KEY(`progressId`))", + "fields": [ + { + "fieldPath": "progressId", + "columnName": "progressId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allModules", + "columnName": "allModules", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "skipContentTags", + "columnName": "skipContentTags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "affectedIds", + "columnName": "affectedIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "progressId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "assignment_filter", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userDomain` TEXT NOT NULL, `userId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `selectedAssignmentFilters` TEXT NOT NULL, `selectedAssignmentStatusFilter` TEXT, `selectedGroupByOption` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userDomain", + "columnName": "userDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedAssignmentFilters", + "columnName": "selectedAssignmentFilters", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "selectedAssignmentStatusFilter", + "columnName": "selectedAssignmentStatusFilter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "selectedGroupByOption", + "columnName": "selectedGroupByOption", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileDownloadProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `filePath` TEXT NOT NULL, PRIMARY KEY(`workerId`))", + "fields": [ + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "filePath", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "workerId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8a50c8d4caed97be61826c69921684e')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json new file mode 100644 index 0000000000..8bfb3ebb8f --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json @@ -0,0 +1,5832 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "e0e8981a53e92176b25c0fb1066137d6", + "entities": [ + { + "tableName": "AssignmentDueDateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `assignmentOverrideId` INTEGER, `dueAt` TEXT, `title` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `isBase` INTEGER NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isBase", + "columnName": "isBase", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `submissionTypesRaw` TEXT NOT NULL, `dueAt` TEXT, `pointsPossible` REAL NOT NULL, `courseId` INTEGER NOT NULL, `isGradeGroupsIndividually` INTEGER NOT NULL, `gradingType` TEXT, `needsGradingCount` INTEGER NOT NULL, `htmlUrl` TEXT, `url` TEXT, `quizId` INTEGER NOT NULL, `isUseRubricForGrading` INTEGER NOT NULL, `rubricSettingsId` INTEGER, `allowedExtensions` TEXT NOT NULL, `submissionId` INTEGER, `assignmentGroupId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `isPeerReviews` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `lockExplanation` TEXT, `discussionTopicHeaderId` INTEGER, `freeFormCriterionComments` INTEGER NOT NULL, `published` INTEGER NOT NULL, `groupCategoryId` INTEGER NOT NULL, `userSubmitted` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `onlyVisibleToOverrides` INTEGER NOT NULL, `anonymousPeerReviews` INTEGER NOT NULL, `moderatedGrading` INTEGER NOT NULL, `anonymousGrading` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `plannerOverrideId` INTEGER, `isStudioEnabled` INTEGER NOT NULL, `inClosedGradingPeriod` INTEGER NOT NULL, `annotatableAttachmentId` INTEGER NOT NULL, `anonymousSubmissions` INTEGER NOT NULL, `omitFromFinalGrade` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentGroupId`) REFERENCES `AssignmentGroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionTypesRaw", + "columnName": "submissionTypesRaw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeGroupsIndividually", + "columnName": "isGradeGroupsIndividually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingType", + "columnName": "gradingType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUseRubricForGrading", + "columnName": "isUseRubricForGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricSettingsId", + "columnName": "rubricSettingsId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "allowedExtensions", + "columnName": "allowedExtensions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPeerReviews", + "columnName": "isPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSubmitted", + "columnName": "userSubmitted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousPeerReviews", + "columnName": "anonymousPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moderatedGrading", + "columnName": "moderatedGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousGrading", + "columnName": "anonymousGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isStudioEnabled", + "columnName": "isStudioEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inClosedGradingPeriod", + "columnName": "inClosedGradingPeriod", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "annotatableAttachmentId", + "columnName": "annotatableAttachmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousSubmissions", + "columnName": "anonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "omitFromFinalGrade", + "columnName": "omitFromFinalGrade", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentGroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentGroupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `position` INTEGER NOT NULL, `groupWeight` REAL NOT NULL, `rules` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupWeight", + "columnName": "groupWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `title` TEXT, `dueAt` INTEGER, `isAllDay` INTEGER NOT NULL, `allDayDate` TEXT, `unlockAt` INTEGER, `lockAt` INTEGER, `courseSectionId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayDate", + "columnName": "allDayDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentRubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `rubricId` TEXT NOT NULL, PRIMARY KEY(`assignmentId`, `rubricId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricId", + "columnName": "rubricId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId", + "rubricId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentScoreStatisticsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `mean` REAL NOT NULL, `min` REAL NOT NULL, `max` REAL NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mean", + "columnName": "mean", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "min", + "columnName": "min", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "max", + "columnName": "max", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentSetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `scoringRangeId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `position` INTEGER NOT NULL, `masteryPathId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`masteryPathId`) REFERENCES `MasteryPathEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringRangeId", + "columnName": "scoringRangeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "masteryPathId", + "columnName": "masteryPathId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "MasteryPathEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "masteryPathId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `originalName` TEXT, `courseCode` TEXT, `startAt` TEXT, `endAt` TEXT, `syllabusBody` TEXT, `hideFinalGrades` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `license` TEXT NOT NULL, `termId` INTEGER, `needsGradingCount` INTEGER NOT NULL, `isApplyAssignmentGroupWeights` INTEGER NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, `isFavorite` INTEGER NOT NULL, `accessRestrictedByDate` INTEGER NOT NULL, `imageUrl` TEXT, `bannerImageUrl` TEXT, `isWeightedGradingPeriods` INTEGER NOT NULL, `hasGradingPeriods` INTEGER NOT NULL, `homePage` TEXT, `restrictEnrollmentsToCourseDate` INTEGER NOT NULL, `workflowState` TEXT, `homeroomCourse` INTEGER NOT NULL, `courseColor` TEXT, `gradingScheme` TEXT, `pointsBasedGradingScheme` INTEGER NOT NULL, `scalingFactor` REAL NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`termId`) REFERENCES `TermEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syllabusBody", + "columnName": "syllabusBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideFinalGrades", + "columnName": "hideFinalGrades", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "termId", + "columnName": "termId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isApplyAssignmentGroupWeights", + "columnName": "isApplyAssignmentGroupWeights", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessRestrictedByDate", + "columnName": "accessRestrictedByDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bannerImageUrl", + "columnName": "bannerImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isWeightedGradingPeriods", + "columnName": "isWeightedGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasGradingPeriods", + "columnName": "hasGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homePage", + "columnName": "homePage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "restrictEnrollmentsToCourseDate", + "columnName": "restrictEnrollmentsToCourseDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeroomCourse", + "columnName": "homeroomCourse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseColor", + "columnName": "courseColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "gradingScheme", + "columnName": "gradingScheme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsBasedGradingScheme", + "columnName": "pointsBasedGradingScheme", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scalingFactor", + "columnName": "scalingFactor", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "TermEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "termId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFilesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`courseId`, `url`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "url" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "CourseGradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `gradingPeriodId` INTEGER NOT NULL, PRIMARY KEY(`courseId`, `gradingPeriodId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`gradingPeriodId`) REFERENCES `GradingPeriodEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "gradingPeriodId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "GradingPeriodEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "gradingPeriodId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseSummary` INTEGER, `restrictQuantitativeData` INTEGER NOT NULL, PRIMARY KEY(`courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseSummary", + "columnName": "courseSummary", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "restrictQuantitativeData", + "columnName": "restrictQuantitativeData", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `fullContentSync` INTEGER NOT NULL, `tabs` TEXT NOT NULL, `fullFileSync` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullContentSync", + "columnName": "fullContentSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullFileSync", + "columnName": "fullFileSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DashboardCardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isK5Subject` INTEGER NOT NULL, `shortName` TEXT, `originalName` TEXT, `courseCode` TEXT, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isK5Subject", + "columnName": "isK5Subject", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionEntryAttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionEntryId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionEntryId`, `remoteFileId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionEntryId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` TEXT, `createdAt` TEXT, `authorId` INTEGER, `description` TEXT, `userId` INTEGER NOT NULL, `parentId` INTEGER NOT NULL, `message` TEXT, `deleted` INTEGER NOT NULL, `totalChildren` INTEGER NOT NULL, `unreadChildren` INTEGER NOT NULL, `ratingCount` INTEGER NOT NULL, `ratingSum` INTEGER NOT NULL, `editorId` INTEGER NOT NULL, `_hasRated` INTEGER NOT NULL, `replyIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChildren", + "columnName": "totalChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadChildren", + "columnName": "unreadChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingCount", + "columnName": "ratingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingSum", + "columnName": "ratingSum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editorId", + "columnName": "editorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_hasRated", + "columnName": "_hasRated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyIds", + "columnName": "replyIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionParticipantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `pronouns` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionTopicHeaderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `discussionType` TEXT, `title` TEXT, `message` TEXT, `htmlUrl` TEXT, `postedDate` INTEGER, `delayedPostDate` INTEGER, `lastReplyDate` INTEGER, `requireInitialPost` INTEGER NOT NULL, `discussionSubentryCount` INTEGER NOT NULL, `readState` TEXT, `unreadCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `assignmentId` INTEGER, `locked` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `pinned` INTEGER NOT NULL, `authorId` INTEGER, `podcastUrl` TEXT, `groupCategoryId` TEXT, `announcement` INTEGER NOT NULL, `permissionId` INTEGER, `published` INTEGER NOT NULL, `allowRating` INTEGER NOT NULL, `onlyGradersCanRate` INTEGER NOT NULL, `sortByRating` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `lockAt` INTEGER, `userCanSeePosts` INTEGER NOT NULL, `specificSections` TEXT, `anonymousState` TEXT, `replyRequiredCount` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`authorId`) REFERENCES `DiscussionParticipantEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`permissionId`) REFERENCES `DiscussionTopicPermissionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionType", + "columnName": "discussionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedDate", + "columnName": "postedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "delayedPostDate", + "columnName": "delayedPostDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastReplyDate", + "columnName": "lastReplyDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "requireInitialPost", + "columnName": "requireInitialPost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionSubentryCount", + "columnName": "discussionSubentryCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readState", + "columnName": "readState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unreadCount", + "columnName": "unreadCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "podcastUrl", + "columnName": "podcastUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "announcement", + "columnName": "announcement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissionId", + "columnName": "permissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowRating", + "columnName": "allowRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyGradersCanRate", + "columnName": "onlyGradersCanRate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortByRating", + "columnName": "sortByRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userCanSeePosts", + "columnName": "userCanSeePosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "specificSections", + "columnName": "specificSections", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "anonymousState", + "columnName": "anonymousState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyRequiredCount", + "columnName": "replyRequiredCount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionParticipantEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "authorId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "DiscussionTopicPermissionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "permissionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `discussionTopicHeaderId` INTEGER NOT NULL, `attach` INTEGER NOT NULL, `update` INTEGER NOT NULL, `delete` INTEGER NOT NULL, `reply` INTEGER NOT NULL, FOREIGN KEY(`discussionTopicHeaderId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attach", + "columnName": "attach", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delete", + "columnName": "delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicHeaderId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicRemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionId`, `remoteFileId`), FOREIGN KEY(`discussionId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionId", + "columnName": "discussionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicSectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionTopicId` INTEGER NOT NULL, `sectionId` INTEGER NOT NULL, PRIMARY KEY(`discussionTopicId`, `sectionId`), FOREIGN KEY(`discussionTopicId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionTopicId", + "columnName": "discussionTopicId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionTopicId", + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `role` TEXT NOT NULL, `type` TEXT NOT NULL, `courseId` INTEGER, `courseSectionId` INTEGER, `enrollmentState` TEXT, `userId` INTEGER NOT NULL, `computedCurrentScore` REAL, `computedFinalScore` REAL, `computedCurrentGrade` TEXT, `computedFinalGrade` TEXT, `multipleGradingPeriodsEnabled` INTEGER NOT NULL, `totalsForAllGradingPeriodsOption` INTEGER NOT NULL, `currentPeriodComputedCurrentScore` REAL, `currentPeriodComputedFinalScore` REAL, `currentPeriodComputedCurrentGrade` TEXT, `currentPeriodComputedFinalGrade` TEXT, `currentGradingPeriodId` INTEGER NOT NULL, `currentGradingPeriodTitle` TEXT, `associatedUserId` INTEGER NOT NULL, `lastActivityAt` INTEGER, `limitPrivilegesToCourseSection` INTEGER NOT NULL, `observedUserId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`observedUserId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseSectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "computedCurrentScore", + "columnName": "computedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedFinalScore", + "columnName": "computedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedCurrentGrade", + "columnName": "computedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "computedFinalGrade", + "columnName": "computedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "multipleGradingPeriodsEnabled", + "columnName": "multipleGradingPeriodsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalsForAllGradingPeriodsOption", + "columnName": "totalsForAllGradingPeriodsOption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPeriodComputedCurrentScore", + "columnName": "currentPeriodComputedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalScore", + "columnName": "currentPeriodComputedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedCurrentGrade", + "columnName": "currentPeriodComputedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalGrade", + "columnName": "currentPeriodComputedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentGradingPeriodId", + "columnName": "currentGradingPeriodId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentGradingPeriodTitle", + "columnName": "currentGradingPeriodTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "associatedUserId", + "columnName": "associatedUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivityAt", + "columnName": "lastActivityAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "limitPrivilegesToCourseSection", + "columnName": "limitPrivilegesToCourseSection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "observedUserId", + "columnName": "observedUserId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "observedUserId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "courseSectionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileFolderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `createdDate` INTEGER, `updatedDate` INTEGER, `unlockDate` INTEGER, `lockDate` INTEGER, `isLocked` INTEGER NOT NULL, `isHidden` INTEGER NOT NULL, `isLockedForUser` INTEGER NOT NULL, `isHiddenForUser` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `size` INTEGER NOT NULL, `contentType` TEXT, `url` TEXT, `displayName` TEXT, `thumbnailUrl` TEXT, `parentFolderId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `filesCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `foldersCount` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `foldersUrl` TEXT, `filesUrl` TEXT, `fullName` TEXT, `forSubmissions` INTEGER NOT NULL, `canUpload` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedDate", + "columnName": "updatedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unlockDate", + "columnName": "unlockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockDate", + "columnName": "lockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLockedForUser", + "columnName": "isLockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHiddenForUser", + "columnName": "isHiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filesCount", + "columnName": "filesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "foldersCount", + "columnName": "foldersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "foldersUrl", + "columnName": "foldersUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesUrl", + "columnName": "filesUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "forSubmissions", + "columnName": "forSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canUpload", + "columnName": "canUpload", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EditDashboardItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `name` TEXT NOT NULL, `isFavorite` INTEGER NOT NULL, `enrollmentState` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ExternalToolAttributesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `url` TEXT, `newTab` INTEGER NOT NULL, `resourceLinkid` TEXT, `contentId` INTEGER, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "newTab", + "columnName": "newTab", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceLinkid", + "columnName": "resourceLinkid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`enrollmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, PRIMARY KEY(`enrollmentId`), FOREIGN KEY(`enrollmentId`) REFERENCES `EnrollmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "enrollmentId", + "columnName": "enrollmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "enrollmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "EnrollmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "enrollmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `startDate` TEXT, `endDate` TEXT, `weight` REAL NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `avatarUrl` TEXT, `isPublic` INTEGER NOT NULL, `membersCount` INTEGER NOT NULL, `joinLevel` TEXT, `courseId` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `role` TEXT, `groupCategoryId` INTEGER NOT NULL, `storageQuotaMb` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `concluded` INTEGER NOT NULL, `canAccess` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinLevel", + "columnName": "joinLevel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "storageQuotaMb", + "columnName": "storageQuotaMb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concluded", + "columnName": "concluded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canAccess", + "columnName": "canAccess", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupUserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LocalFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `createdDate` INTEGER NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MasteryPathAssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `overrideId` INTEGER NOT NULL, `assignmentSetId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentSetId`) REFERENCES `AssignmentSetEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "overrideId", + "columnName": "overrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentSetId", + "columnName": "assignmentSetId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentSetEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MasteryPathEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `selectedSetId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedSetId", + "columnName": "selectedSetId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleContentDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pointsPossible` TEXT, `dueAt` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `moduleId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `title` TEXT, `indent` INTEGER NOT NULL, `type` TEXT, `htmlUrl` TEXT, `url` TEXT, `published` INTEGER, `contentId` INTEGER NOT NULL, `externalUrl` TEXT, `pageUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`moduleId`) REFERENCES `ModuleObjectEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "indent", + "columnName": "indent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageUrl", + "columnName": "pageUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleObjectEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleObjectEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `name` TEXT, `unlockAt` TEXT, `sequentialProgress` INTEGER NOT NULL, `prerequisiteIds` TEXT, `state` TEXT, `completedAt` TEXT, `published` INTEGER, `itemCount` INTEGER NOT NULL, `itemsUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sequentialProgress", + "columnName": "sequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prerequisiteIds", + "columnName": "prerequisiteIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "completedAt", + "columnName": "completedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemCount", + "columnName": "itemCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemsUrl", + "columnName": "itemsUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "NeedsGradingCountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sectionId` INTEGER NOT NULL, `needsGradingCount` INTEGER NOT NULL, PRIMARY KEY(`sectionId`), FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `createdAt` INTEGER, `updatedAt` INTEGER, `hideFromStudents` INTEGER NOT NULL, `status` TEXT, `body` TEXT, `frontPage` INTEGER NOT NULL, `published` INTEGER NOT NULL, `editingRoles` TEXT, `htmlUrl` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hideFromStudents", + "columnName": "hideFromStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "frontPage", + "columnName": "frontPage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editingRoles", + "columnName": "editingRoles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, `markedComplete` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "markedComplete", + "columnName": "markedComplete", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `displayName` TEXT, `fileName` TEXT, `contentType` TEXT, `url` TEXT, `size` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `unlockAt` TEXT, `locked` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `lockAt` TEXT, `hiddenForUser` INTEGER NOT NULL, `thumbnailUrl` TEXT, `modifiedAt` TEXT, `lockedForUser` INTEGER NOT NULL, `previewUrl` TEXT, `lockExplanation` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hiddenForUser", + "columnName": "hiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "modifiedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RubricCriterionAssessmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `assignmentId` INTEGER NOT NULL, `ratingId` TEXT, `points` REAL, `comments` TEXT, PRIMARY KEY(`id`, `assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingId", + "columnName": "ratingId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "comments", + "columnName": "comments", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `criterionUseRange` INTEGER NOT NULL, `ignoreForScoring` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "criterionUseRange", + "columnName": "criterionUseRange", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoreForScoring", + "columnName": "ignoreForScoring", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionRatingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `rubricCriterionId` TEXT NOT NULL, PRIMARY KEY(`id`, `rubricCriterionId`), FOREIGN KEY(`rubricCriterionId`) REFERENCES `RubricCriterionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rubricCriterionId", + "columnName": "rubricCriterionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "rubricCriterionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "RubricCriterionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "rubricCriterionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `pointsPossible` REAL NOT NULL, `title` TEXT NOT NULL, `isReusable` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `isReadOnly` INTEGER NOT NULL, `freeFormCriterionComments` INTEGER NOT NULL, `hideScoreTotal` INTEGER NOT NULL, `hidePoints` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isReusable", + "columnName": "isReusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isReadOnly", + "columnName": "isReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideScoreTotal", + "columnName": "hideScoreTotal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidePoints", + "columnName": "hidePoints", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemAssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentOverrideId` INTEGER NOT NULL, `scheduleItemId` TEXT NOT NULL, PRIMARY KEY(`assignmentOverrideId`, `scheduleItemId`), FOREIGN KEY(`assignmentOverrideId`) REFERENCES `AssignmentOverrideEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`scheduleItemId`) REFERENCES `ScheduleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduleItemId", + "columnName": "scheduleItemId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentOverrideId", + "scheduleItemId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentOverrideEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentOverrideId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ScheduleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "scheduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `description` TEXT, `startAt` TEXT, `endAt` TEXT, `isAllDay` INTEGER NOT NULL, `allDayAt` TEXT, `locationAddress` TEXT, `locationName` TEXT, `htmlUrl` TEXT, `contextCode` TEXT, `effectiveContextCode` TEXT, `isHidden` INTEGER NOT NULL, `importantDates` INTEGER NOT NULL, `assignmentId` INTEGER, `type` TEXT NOT NULL, `itemType` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayAt", + "columnName": "allDayAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationAddress", + "columnName": "locationAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationName", + "columnName": "locationName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextCode", + "columnName": "contextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effectiveContextCode", + "columnName": "effectiveContextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "importantDates", + "columnName": "importantDates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemType", + "columnName": "itemType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER, `startAt` TEXT, `endAt` TEXT, `totalStudents` INTEGER NOT NULL, `restrictEnrollmentsToSectionDates` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "totalStudents", + "columnName": "totalStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "restrictEnrollmentsToSectionDates", + "columnName": "restrictEnrollmentsToSectionDates", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionDiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`submissionId` INTEGER NOT NULL, `discussionEntryId` INTEGER NOT NULL, PRIMARY KEY(`submissionId`, `discussionEntryId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "submissionId", + "discussionEntryId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `attempt` INTEGER NOT NULL, `submittedAt` INTEGER, `commentCreated` INTEGER, `mediaContentType` TEXT, `mediaCommentUrl` TEXT, `mediaCommentDisplay` TEXT, `body` TEXT, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, `workflowState` TEXT, `submissionType` TEXT, `previewUrl` TEXT, `url` TEXT, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `mediaCommentId` TEXT, `assignmentId` INTEGER NOT NULL, `userId` INTEGER, `graderId` INTEGER, `groupId` INTEGER, `pointsDeducted` REAL, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `postedAt` INTEGER, `gradingPeriodId` INTEGER, `customGradeStatusId` INTEGER, `hasSubAssignmentSubmissions` INTEGER NOT NULL, PRIMARY KEY(`id`, `attempt`), FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submittedAt", + "columnName": "submittedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "commentCreated", + "columnName": "commentCreated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaContentType", + "columnName": "mediaContentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentUrl", + "columnName": "mediaCommentUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentDisplay", + "columnName": "mediaCommentDisplay", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionType", + "columnName": "submissionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "graderId", + "columnName": "graderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pointsDeducted", + "columnName": "pointsDeducted", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasSubAssignmentSubmissions", + "columnName": "hasSubAssignmentSubmissions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "attempt" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `autoSyncEnabled` INTEGER NOT NULL, `syncFrequency` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoSyncEnabled", + "columnName": "autoSyncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncFrequency", + "columnName": "syncFrequency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifiOnly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TabEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `label` TEXT, `type` TEXT NOT NULL, `htmlUrl` TEXT, `externalUrl` TEXT, `visibility` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL, `ltiUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ltiUrl", + "columnName": "ltiUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "TermEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `startAt` TEXT, `endAt` TEXT, `isGroupTerm` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGroupTerm", + "columnName": "isGroupTerm", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserCalendarEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ics` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ics", + "columnName": "ics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `shortName` TEXT, `loginId` TEXT, `avatarUrl` TEXT, `primaryEmail` TEXT, `email` TEXT, `sortableName` TEXT, `bio` TEXT, `enrollmentIndex` INTEGER NOT NULL, `lastLogin` TEXT, `locale` TEXT, `effective_locale` TEXT, `pronouns` TEXT, `k5User` INTEGER NOT NULL, `rootAccount` TEXT, `isFakeStudent` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginId", + "columnName": "loginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "primaryEmail", + "columnName": "primaryEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortableName", + "columnName": "sortableName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bio", + "columnName": "bio", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enrollmentIndex", + "columnName": "enrollmentIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastLogin", + "columnName": "lastLogin", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effective_locale", + "columnName": "effective_locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "k5User", + "columnName": "k5User", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rootAccount", + "columnName": "rootAccount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFakeStudent", + "columnName": "isFakeStudent", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "QuizEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `mobileUrl` TEXT, `htmlUrl` TEXT, `description` TEXT, `quizType` TEXT, `assignmentGroupId` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `questionCount` INTEGER NOT NULL, `pointsPossible` TEXT, `isLockQuestionsAfterAnswering` INTEGER NOT NULL, `dueAt` TEXT, `timeLimit` INTEGER NOT NULL, `shuffleAnswers` INTEGER NOT NULL, `showCorrectAnswers` INTEGER NOT NULL, `scoringPolicy` TEXT, `accessCode` TEXT, `ipFilter` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `hideResults` TEXT, `showCorrectAnswersAt` TEXT, `hideCorrectAnswersAt` TEXT, `unlockAt` TEXT, `oneTimeResults` INTEGER NOT NULL, `lockAt` TEXT, `questionTypes` TEXT NOT NULL, `hasAccessCode` INTEGER NOT NULL, `oneQuestionAtATime` INTEGER NOT NULL, `requireLockdownBrowser` INTEGER NOT NULL, `requireLockdownBrowserForResults` INTEGER NOT NULL, `allowAnonymousSubmissions` INTEGER NOT NULL, `published` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `isOnlyVisibleToOverrides` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mobileUrl", + "columnName": "mobileUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizType", + "columnName": "quizType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionCount", + "columnName": "questionCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLockQuestionsAfterAnswering", + "columnName": "isLockQuestionsAfterAnswering", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeLimit", + "columnName": "timeLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shuffleAnswers", + "columnName": "shuffleAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showCorrectAnswers", + "columnName": "showCorrectAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringPolicy", + "columnName": "scoringPolicy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accessCode", + "columnName": "accessCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ipFilter", + "columnName": "ipFilter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideResults", + "columnName": "hideResults", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showCorrectAnswersAt", + "columnName": "showCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideCorrectAnswersAt", + "columnName": "hideCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oneTimeResults", + "columnName": "oneTimeResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "questionTypes", + "columnName": "questionTypes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasAccessCode", + "columnName": "hasAccessCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneQuestionAtATime", + "columnName": "oneQuestionAtATime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowser", + "columnName": "requireLockdownBrowser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowserForResults", + "columnName": "requireLockdownBrowserForResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowAnonymousSubmissions", + "columnName": "allowAnonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnlyVisibleToOverrides", + "columnName": "isOnlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockInfoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `modulePrerequisiteNames` TEXT, `unlockAt` TEXT, `lockedModuleId` INTEGER, `assignmentId` INTEGER, `moduleId` INTEGER, `pageId` INTEGER, FOREIGN KEY(`moduleId`) REFERENCES `ModuleContentDetailsEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`pageId`) REFERENCES `PageEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modulePrerequisiteNames", + "columnName": "modulePrerequisiteNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleContentDetailsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "PageEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockedModuleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `unlockAt` TEXT, `isRequireSequentialProgress` INTEGER NOT NULL, `lockInfoId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`lockInfoId`) REFERENCES `LockInfoEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isRequireSequentialProgress", + "columnName": "isRequireSequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockInfoId", + "columnName": "lockInfoId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockInfoEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockInfoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleNameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `lockedModuleId` INTEGER NOT NULL, FOREIGN KEY(`lockedModuleId`) REFERENCES `LockedModuleEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockedModuleEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockedModuleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleCompletionRequirementEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT, `minScore` REAL NOT NULL, `maxScore` REAL NOT NULL, `completed` INTEGER, `moduleId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "minScore", + "columnName": "minScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxScore", + "columnName": "maxScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `fileName` TEXT, `courseId` INTEGER NOT NULL, `url` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "ConferenceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `conferenceKey` TEXT, `conferenceType` TEXT, `description` TEXT, `duration` INTEGER NOT NULL, `endedAt` INTEGER, `hasAdvancedSettings` INTEGER NOT NULL, `joinUrl` TEXT, `longRunning` INTEGER NOT NULL, `startedAt` INTEGER, `title` TEXT, `url` TEXT, `contextType` TEXT NOT NULL, `contextId` INTEGER NOT NULL, `record` INTEGER, `users` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conferenceKey", + "columnName": "conferenceKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conferenceType", + "columnName": "conferenceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAdvancedSettings", + "columnName": "hasAdvancedSettings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinUrl", + "columnName": "joinUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longRunning", + "columnName": "longRunning", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startedAt", + "columnName": "startedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "record", + "columnName": "record", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "users", + "columnName": "users", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ConferenceRecordingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recordingId` TEXT NOT NULL, `conferenceId` INTEGER NOT NULL, `createdAtMillis` INTEGER NOT NULL, `durationMinutes` INTEGER NOT NULL, `playbackUrl` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`recordingId`), FOREIGN KEY(`conferenceId`) REFERENCES `ConferenceEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "recordingId", + "columnName": "recordingId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conferenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtMillis", + "columnName": "createdAtMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durationMinutes", + "columnName": "durationMinutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playbackUrl", + "columnName": "playbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recordingId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ConferenceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "conferenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFeaturesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `features` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionCommentId`) REFERENCES `SubmissionCommentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionCommentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionCommentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `submissionId` INTEGER NOT NULL, `attemptId` INTEGER NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "DiscussionTopicEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unreadEntries` TEXT NOT NULL, `participantIds` TEXT NOT NULL, `viewIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadEntries", + "columnName": "unreadEntries", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantIds", + "columnName": "participantIds", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewIds", + "columnName": "viewIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CourseSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `tabs` TEXT NOT NULL, `additionalFilesStarted` INTEGER NOT NULL, `progressState` TEXT NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalFilesStarted", + "columnName": "additionalFilesStarted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalFile", + "columnName": "additionalFile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncProgressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "StudioMediaProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ltiLaunchId` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "ltiLaunchId", + "columnName": "ltiLaunchId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomGradeStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CheckpointEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `assignmentId` INTEGER NOT NULL, `name` TEXT, `tag` TEXT, `pointsPossible` REAL, `dueAt` TEXT, `onlyVisibleToOverrides` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubAssignmentSubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `submissionId` INTEGER NOT NULL, `submissionAttempt` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `latePolicyStatus` TEXT, `customGradeStatusId` INTEGER, `subAssignmentTag` TEXT, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `userId` INTEGER NOT NULL, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, FOREIGN KEY(`submissionId`, `submissionAttempt`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionAttempt", + "columnName": "submissionAttempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latePolicyStatus", + "columnName": "latePolicyStatus", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subAssignmentTag", + "columnName": "subAssignmentTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "submissionAttempt" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e0e8981a53e92176b25c0fb1066137d6')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt new file mode 100644 index 0000000000..c5518f2d68 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2025 - 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.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Checkpoint +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentEntity +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity +import com.instructure.pandautils.room.offline.entities.CheckpointEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CheckpointDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var checkpointDao: CheckpointDao + private lateinit var assignmentDao: AssignmentDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + checkpointDao = db.checkpointDao() + assignmentDao = db.assignmentDao() + assignmentGroupDao = db.assignmentGroupDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertAndFind() = runTest { + setupAssignment(1L, 1L) + + val checkpoint = CheckpointEntity( + assignmentId = 1L, + name = "Checkpoint 1", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insert(checkpoint) + + val result = checkpointDao.findByAssignmentId(1L) + assertEquals(1, result.size) + assertEquals("Checkpoint 1", result[0].name) + assertEquals("reply_to_topic", result[0].tag) + assertEquals(10.0, result[0].pointsPossible) + } + + @Test + fun testInsertMultipleCheckpoints() = runTest { + setupAssignment(1L, 1L) + + val checkpoint1 = CheckpointEntity( + assignmentId = 1L, + name = "Reply to Topic", + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + val checkpoint2 = CheckpointEntity( + assignmentId = 1L, + name = "Required Replies", + tag = "reply_to_entry", + pointsPossible = 5.0, + dueAt = "2025-10-20T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insertAll(listOf(checkpoint1, checkpoint2)) + + val result = checkpointDao.findByAssignmentId(1L) + assertEquals(2, result.size) + assertTrue(result.any { it.tag == "reply_to_topic" }) + assertTrue(result.any { it.tag == "reply_to_entry" }) + } + + @Test + fun testDeleteByAssignmentId() = runTest { + setupAssignment(1L, 1L) + + val checkpoint = CheckpointEntity( + assignmentId = 1L, + name = "Checkpoint 1", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insert(checkpoint) + checkpointDao.deleteByAssignmentId(1L) + + val result = checkpointDao.findByAssignmentId(1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testCascadeDelete() = runTest { + setupAssignment(1L, 1L) + + val checkpoint = CheckpointEntity( + assignmentId = 1L, + name = "Checkpoint 1", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insert(checkpoint) + + val assignmentEntity = assignmentDao.findById(1L)!! + assignmentDao.delete(assignmentEntity) + + val result = checkpointDao.findByAssignmentId(1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testToApiModel() { + val checkpointEntity = CheckpointEntity( + assignmentId = 1L, + name = "Reply to Topic", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = true, + lockAt = "2025-10-22T23:59:59Z", + unlockAt = "2025-10-10T00:00:00Z" + ) + + val checkpoint = checkpointEntity.toApiModel() + + assertEquals("Reply to Topic", checkpoint.name) + assertEquals("reply_to_topic", checkpoint.tag) + assertEquals(10.0, checkpoint.pointsPossible) + assertEquals("2025-10-15T23:59:59Z", checkpoint.dueAt) + assertEquals(true, checkpoint.onlyVisibleToOverrides) + assertEquals("2025-10-22T23:59:59Z", checkpoint.lockAt) + assertEquals("2025-10-10T00:00:00Z", checkpoint.unlockAt) + } + + @Test + fun testConstructorFromApiModel() { + val checkpoint = Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + overrides = null, + onlyVisibleToOverrides = true, + lockAt = "2025-10-22T23:59:59Z", + unlockAt = "2025-10-10T00:00:00Z" + ) + + val entity = CheckpointEntity(checkpoint, 1L) + + assertEquals(1L, entity.assignmentId) + assertEquals("Reply to Topic", entity.name) + assertEquals("reply_to_topic", entity.tag) + assertEquals(10.0, entity.pointsPossible) + assertEquals("2025-10-15T23:59:59Z", entity.dueAt) + assertEquals(true, entity.onlyVisibleToOverrides) + assertEquals("2025-10-22T23:59:59Z", entity.lockAt) + assertEquals("2025-10-10T00:00:00Z", entity.unlockAt) + } + + private suspend fun setupAssignment(assignmentId: Long, courseId: Long) { + val courseEntity = CourseEntity(Course(id = courseId)) + courseDao.insert(courseEntity) + + val assignmentGroupEntity = AssignmentGroupEntity(AssignmentGroup(id = 1L), courseId) + assignmentGroupDao.insert(assignmentGroupEntity) + + val assignmentEntity = AssignmentEntity( + Assignment(id = assignmentId, name = "Test Assignment", assignmentGroupId = 1L, courseId = courseId), + null, null, null, null + ) + assignmentDao.insert(assignmentEntity) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt new file mode 100644 index 0000000000..7596709d0b --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2025 - 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.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.SubAssignmentSubmission +import com.instructure.canvasapi2.models.Submission +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentEntity +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.SubAssignmentSubmissionEntity +import com.instructure.pandautils.room.offline.entities.SubmissionEntity +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SubAssignmentSubmissionDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var subAssignmentSubmissionDao: SubAssignmentSubmissionDao + private lateinit var submissionDao: SubmissionDao + private lateinit var assignmentDao: AssignmentDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + subAssignmentSubmissionDao = db.subAssignmentSubmissionDao() + submissionDao = db.submissionDao() + assignmentDao = db.assignmentDao() + assignmentGroupDao = db.assignmentGroupDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertAndFind() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignmentSubmission = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insert(subAssignmentSubmission) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertEquals(1, result.size) + assertEquals("A", result[0].grade) + assertEquals(10.0, result[0].score) + assertEquals("reply_to_topic", result[0].subAssignmentTag) + } + + @Test + fun testInsertMultipleSubAssignmentSubmissions() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignment1 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 5.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 5.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val subAssignment2 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "B", + score = 4.0, + late = true, + excused = false, + missing = false, + latePolicyStatus = "late", + customGradeStatusId = null, + subAssignmentTag = "reply_to_entry", + enteredScore = 5.0, + enteredGrade = "B", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insertAll(listOf(subAssignment1, subAssignment2)) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertEquals(2, result.size) + assertTrue(result.any { it.subAssignmentTag == "reply_to_topic" && it.score == 5.0 }) + assertTrue(result.any { it.subAssignmentTag == "reply_to_entry" && it.late }) + } + + @Test + fun testDeleteBySubmissionIdAndAttempt() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignmentSubmission = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insert(subAssignmentSubmission) + subAssignmentSubmissionDao.deleteBySubmissionIdAndAttempt(1L, 1L) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testCascadeDelete() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignmentSubmission = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insert(subAssignmentSubmission) + + val submissions = submissionDao.findById(1L) + val submissionEntity = submissions.first { it.id == 1L && it.attempt == 1L } + submissionDao.delete(submissionEntity) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testMultipleAttempts() = runTest { + setupSubmission(1L, 1L, 1L) + + val submission2 = SubmissionEntity( + Submission(id = 1L, assignmentId = 1L, attempt = 2L), + null, + null + ) + submissionDao.insert(submission2) + + val subAssignment1 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "B", + score = 8.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 8.0, + enteredGrade = "B", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val subAssignment2 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 2L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insertAll(listOf(subAssignment1, subAssignment2)) + + val attempt1Results = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + val attempt2Results = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 2L) + + assertEquals(1, attempt1Results.size) + assertEquals(8.0, attempt1Results[0].score) + assertEquals(1, attempt2Results.size) + assertEquals(10.0, attempt2Results[0].score) + } + + @Test + fun testToApiModel() { + val entity = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = true, + excused = false, + missing = false, + latePolicyStatus = "late", + customGradeStatusId = 123L, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val apiModel = entity.toApiModel() + + assertEquals("A", apiModel.grade) + assertEquals(10.0, apiModel.score) + assertEquals(true, apiModel.late) + assertEquals(false, apiModel.excused) + assertEquals(false, apiModel.missing) + assertEquals("late", apiModel.latePolicyStatus) + assertEquals(123L, apiModel.customGradeStatusId) + assertEquals("reply_to_topic", apiModel.subAssignmentTag) + assertEquals(10.0, apiModel.enteredScore) + assertEquals("A", apiModel.enteredGrade) + assertEquals(1L, apiModel.userId) + assertEquals(true, apiModel.isGradeMatchesCurrentSubmission) + } + + @Test + fun testConstructorFromApiModel() { + val apiModel = SubAssignmentSubmission( + grade = "A", + score = 10.0, + late = true, + excused = false, + missing = false, + latePolicyStatus = "late", + customGradeStatusId = 123L, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val entity = SubAssignmentSubmissionEntity(apiModel, 1L, 2L) + + assertEquals(1L, entity.submissionId) + assertEquals(2L, entity.submissionAttempt) + assertEquals("A", entity.grade) + assertEquals(10.0, entity.score) + assertEquals(true, entity.late) + assertEquals("reply_to_topic", entity.subAssignmentTag) + } + + private suspend fun setupSubmission(submissionId: Long, assignmentId: Long, courseId: Long) { + val courseEntity = CourseEntity(Course(id = courseId)) + courseDao.insert(courseEntity) + + val assignmentGroupEntity = AssignmentGroupEntity(AssignmentGroup(id = 1L), courseId) + assignmentGroupDao.insert(assignmentGroupEntity) + + val assignmentEntity = AssignmentEntity( + Assignment(id = assignmentId, name = "Test Assignment", assignmentGroupId = 1L, courseId = courseId), + null, null, null, null + ) + assignmentDao.insert(assignmentEntity) + + val submissionEntity = SubmissionEntity( + Submission(id = submissionId, assignmentId = assignmentId, attempt = 1L), + null, + null + ) + submissionDao.insert(submissionEntity) + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt index 4f4433f3ed..4a2b9f310d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt @@ -30,6 +30,8 @@ import com.instructure.pandautils.room.offline.daos.AssignmentScoreStatisticsDao import com.instructure.pandautils.room.offline.daos.AssignmentSetDao import com.instructure.pandautils.room.offline.daos.AttachmentDao import com.instructure.pandautils.room.offline.daos.AuthorDao +import com.instructure.pandautils.room.offline.daos.CheckpointDao +import com.instructure.pandautils.room.offline.daos.SubAssignmentSubmissionDao import com.instructure.pandautils.room.offline.daos.ConferenceDao import com.instructure.pandautils.room.offline.daos.ConferenceRecodingDao import com.instructure.pandautils.room.offline.daos.CourseDao @@ -318,6 +320,7 @@ class OfflineModule { lockInfoFacade: LockInfoFacade, rubricCriterionRatingDao: RubricCriterionRatingDao, assignmentRubricCriterionDao: AssignmentRubricCriterionDao, + checkpointDao: CheckpointDao, offlineDatabase: OfflineDatabase ): AssignmentFacade { return AssignmentFacade( @@ -332,6 +335,7 @@ class OfflineModule { lockInfoFacade, rubricCriterionRatingDao, assignmentRubricCriterionDao, + checkpointDao, offlineDatabase ) } @@ -345,11 +349,13 @@ class OfflineModule { submissionCommentDao: SubmissionCommentDao, attachmentDao: AttachmentDao, authorDao: AuthorDao, - rubricCriterionAssessmentDao: RubricCriterionAssessmentDao + rubricCriterionAssessmentDao: RubricCriterionAssessmentDao, + subAssignmentSubmissionDao: SubAssignmentSubmissionDao ): SubmissionFacade { return SubmissionFacade( submissionDao, groupDao, mediaCommentDao, userDao, - submissionCommentDao, attachmentDao, authorDao, rubricCriterionAssessmentDao + submissionCommentDao, attachmentDao, authorDao, rubricCriterionAssessmentDao, + subAssignmentSubmissionDao ) } @@ -643,4 +649,14 @@ class OfflineModule { fun provideCustomGradeStatusDao(database: OfflineDatabase): CustomGradeStatusDao { return database.customGradeStatusDao() } + + @Provides + fun provideCheckpointDao(database: OfflineDatabase): CheckpointDao { + return database.checkpointDao() + } + + @Provides + fun provideSubAssignmentSubmissionDao(database: OfflineDatabase): SubAssignmentSubmissionDao { + return database.subAssignmentSubmissionDao() + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt index 65abd75138..84ae5f008d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt @@ -53,6 +53,7 @@ import com.instructure.pandautils.analytics.SCREEN_VIEW_ASSIGNMENT_DETAILS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.base.BaseCanvasFragment import com.instructure.pandautils.databinding.FragmentAssignmentDetailsBinding +import com.instructure.pandautils.features.assignments.details.composables.DueDateReminderLayout import com.instructure.pandautils.features.reminder.composables.ReminderView import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget import com.instructure.pandautils.navigation.WebViewRouter @@ -146,16 +147,22 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo viewModel.course.value?.let { viewModel.updateReminderColor(assignmentDetailsBehaviour.getThemeColor(it)) } - binding?.reminderComposeView?.setContent { - val state by viewModel.reminderViewState.collectAsState() - ReminderView( - viewState = state, - onAddClick = { checkAlarmPermission() }, + + binding?.dueComposeView?.setContent { + val states = viewModel.dueDateReminderViewStates + DueDateReminderLayout( + states, + onAddClick = { tag -> checkAlarmPermission(tag) }, onRemoveClick = { reminderId -> - viewModel.showDeleteReminderConfirmationDialog(requireContext(), reminderId, assignmentDetailsBehaviour.dialogColor) + viewModel.showDeleteReminderConfirmationDialog( + requireContext(), + reminderId, + assignmentDetailsBehaviour.dialogColor + ) } ) } + return binding?.root } @@ -367,14 +374,14 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo } } - private fun checkAlarmPermission() { + private fun checkAlarmPermission(tag: String? = null) { val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as AlarmManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && requireActivity().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { viewModel.checkingNotificationPermission = true notificationsPermissionContract.launch(Manifest.permission.POST_NOTIFICATIONS) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (alarmManager.canScheduleExactAlarms()) { - viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor) + viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor, tag) } else { viewModel.checkingReminderPermission = true startActivity( @@ -385,7 +392,7 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo ) } } else { - viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor) + viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor, tag) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt index c5a904087d..a91907067a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt @@ -40,8 +40,7 @@ data class AssignmentDetailsViewData( val discussionHeaderViewData: DiscussionHeaderViewData? = null, val quizDetails: QuizViewViewData? = null, val attemptsViewData: AttemptsViewData? = null, - @Bindable var hasDraft: Boolean = false, - @Bindable var reminders: List = emptyList() + @Bindable var hasDraft: Boolean = false ) : BaseObservable() { val firstAttemptOrNull = attempts.firstOrNull() val noDescriptionVisible = description.isEmpty() && !fullLocked diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt index 654d3c0392..fcbe41d10b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt @@ -22,6 +22,7 @@ import android.content.Context import android.content.res.Resources import android.net.Uri import androidx.annotation.ColorInt +import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.graphics.Color import androidx.fragment.app.FragmentActivity import androidx.lifecycle.LiveData @@ -69,6 +70,7 @@ import com.instructure.pandautils.utils.HtmlContentFormatter import com.instructure.pandautils.utils.getSubmissionStateLabel import com.instructure.pandautils.utils.isAudioVisualExtension import com.instructure.pandautils.utils.orDefault +import com.instructure.pandautils.utils.orderedCheckpoints import com.instructure.pandautils.utils.toFormattedString import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -134,8 +136,11 @@ class AssignmentDetailsViewModel @Inject constructor( private var selectedSubmission: Submission? = null - private val _reminderViewState = MutableStateFlow(ReminderViewState()) - val reminderViewState = _reminderViewState.asStateFlow() + private var reminderEntities: List = emptyList() + private var themeColor: Color? = null + private val _dueDateReminderViewStates = mutableStateListOf() + val dueDateReminderViewStates: List + get() = _dueDateReminderViewStates var checkingReminderPermission = false var checkingNotificationPermission = false @@ -156,15 +161,29 @@ class AssignmentDetailsViewModel @Inject constructor( loadData() reminderManager.observeRemindersLiveData(apiPrefs.user?.id.orDefault(), assignmentId) { reminderEntities -> - _data.value?.reminders = mapReminders(reminderEntities) - _reminderViewState.update { it.copy( - reminders = reminderEntities.map { ReminderItem(it.id, it.text, Date(it.time)) }, - dueDate = assignment?.dueDate - ) } - _data.value?.notifyPropertyChanged(BR.reminders) + this.reminderEntities = reminderEntities + updateDueDatesViewState(reminderEntities) } } + private fun updateDueDatesViewState(reminderEntities: List) { + for (i in 0.._dueDateReminderViewStates.lastIndex) { + val tag = _dueDateReminderViewStates[i].tag + _dueDateReminderViewStates[i] = _dueDateReminderViewStates[i].copy( + reminders = getReminderItems(tag) + ) + } + } + + private fun getReminderItems(tag: String? = null): List { + return reminderEntities + .filter { it.tag == tag } + .sortedBy { it.time } + .map { + ReminderItem(it.id, it.text, Date(it.time)) + } + } + fun getVideoUri(fragment: FragmentActivity): Uri? = submissionHandler.getVideoUri(fragment) override fun onCleared() { @@ -227,9 +246,44 @@ class AssignmentDetailsViewModel @Inject constructor( isAssignmentEnhancementEnabled = assignmentDetailsRepository.isAssignmentEnhancementEnabled(courseId.orDefault(), forceNetwork) assignment = assignmentResult - _reminderViewState.update { it.copy( - dueDate = if (assignment?.submission?.excused.orDefault()) null else assignment?.dueDate - ) } + + if (assignment?.checkpoints?.isNotEmpty() == true) { + _dueDateReminderViewStates.clear() + assignment?.orderedCheckpoints?.forEach { checkpoint -> + val dueLabel = when (checkpoint.tag) { + Const.REPLY_TO_TOPIC -> application.getString(R.string.reply_to_topic_due) + Const.REPLY_TO_ENTRY -> { + application.getString( + R.string.additional_replies_due, + assignment?.discussionTopicHeader?.replyRequiredCount ?: 0 + ) + } + + else -> application.getString(R.string.dueLabel) + } + val subAssignment = assignment?.submission?.subAssignmentSubmissions?.firstOrNull { it.subAssignmentTag == checkpoint.tag } + _dueDateReminderViewStates.add( + ReminderViewState( + dueLabel = dueLabel, + themeColor = themeColor, + dueDate = if (subAssignment?.excused.orDefault()) null else checkpoint.dueDate, + tag = checkpoint.tag, + reminders = getReminderItems(checkpoint.tag) + ) + ) + } + } else { + _dueDateReminderViewStates.clear() + _dueDateReminderViewStates.add( + ReminderViewState( + dueLabel = application.getString(R.string.dueLabel), + themeColor = themeColor, + dueDate = if (assignment?.submission?.excused.orDefault()) null else assignment?.dueDate, + tag = null, + reminders = getReminderItems() + ) + ) + } _data.postValue(getViewData(assignmentResult, hasDraft)) _state.postValue(ViewState.Success) @@ -478,8 +532,7 @@ class AssignmentDetailsViewModel @Inject constructor( discussionHeaderViewData = discussionHeaderViewData, quizDetails = quizViewViewData, attemptsViewData = attemptsViewData, - hasDraft = hasDraft, - reminders = _data.value?.reminders.orEmpty(), + hasDraft = hasDraft ) } @@ -628,28 +681,34 @@ class AssignmentDetailsViewModel @Inject constructor( } fun updateReminderColor(@ColorInt color: Int) { - _reminderViewState.update { it.copy(themeColor = Color(color)) } + themeColor = Color(color) + for (i in 0.._dueDateReminderViewStates.lastIndex) { + _dueDateReminderViewStates[i] = _dueDateReminderViewStates[i].copy(themeColor = themeColor) + } } - fun showCreateReminderDialog(context: Context, @ColorInt color: Int) { + fun showCreateReminderDialog(context: Context, @ColorInt color: Int, tag: String? = null) { assignment?.let { assignment -> viewModelScope.launch { + val dueDate = _dueDateReminderViewStates.firstOrNull { it.tag == tag }?.dueDate when { - assignment.dueDate == null -> reminderManager.showCustomReminderDialog( + dueDate == null -> reminderManager.showCustomReminderDialog( context, apiPrefs.user?.id.orDefault(), assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate + dueDate, + tag ) - assignment.dueDate?.before(Date()).orDefault() -> reminderManager.showCustomReminderDialog( + dueDate.before(Date()).orDefault() -> reminderManager.showCustomReminderDialog( context, apiPrefs.user?.id.orDefault(), assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate + dueDate, + tag ) else -> reminderManager.showBeforeDueDateReminderDialog( context, @@ -657,8 +716,9 @@ class AssignmentDetailsViewModel @Inject constructor( assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate ?: Date(), - color + dueDate ?: Date(), + color, + tag ) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt new file mode 100644 index 0000000000..b7c7705ac0 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2025 - 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.pandautils.features.assignments.details.composables + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.instructure.pandares.R +import com.instructure.pandautils.compose.composables.CanvasDivider +import com.instructure.pandautils.features.reminder.ReminderViewState +import com.instructure.pandautils.features.reminder.composables.ReminderView +import com.instructure.pandautils.utils.toFormattedString + +@Composable +fun DueDateReminderLayout( + reminderViewStates: List, + onAddClick: (String?) -> Unit, + onRemoveClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + reminderViewStates.forEachIndexed { index, reminderViewState -> + DueDateBlock(reminderViewState, index) + ReminderView( + viewState = reminderViewState, + onAddClick = onAddClick, + onRemoveClick = onRemoveClick, + modifier = Modifier.testTag("reminderView-$index") + ) + CanvasDivider() + } + } +} + +@Composable +private fun DueDateBlock( + reminderViewState: ReminderViewState, + position: Int +) { + Text( + modifier = Modifier + .padding(top = 24.dp, start = 16.dp, end = 16.dp) + .semantics { heading() } + .testTag("dueDateHeaderText-$position"), + text = reminderViewState.dueLabel ?: stringResource(id = R.string.dueLabel), + color = colorResource(id = R.color.textDark), + fontSize = 14.sp + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + modifier = Modifier + .padding(bottom = 14.dp, start = 16.dp, end = 16.dp) + .testTag("dueDateText-$position"), + text = reminderViewState.dueDate?.toFormattedString() ?: stringResource(R.string.toDoNoDueDate), + color = colorResource(id = R.color.textDarkest), + fontSize = 16.sp + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderManager.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderManager.kt index b82889fb91..bf2d3d48d8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderManager.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderManager.kt @@ -53,17 +53,18 @@ class ReminderManager( contentName: String, contentHtmlUrl: String, dueDate: Date, - @ColorInt color: Int + @ColorInt color: Int, + tag: String? = null ) { showBeforeDueDateReminderDialog(context, dueDate, color).collect { calendar -> - createReminder(context, calendar, userId, contentId, contentName, contentHtmlUrl, dueDate) + createReminder(context, calendar, userId, contentId, contentName, contentHtmlUrl, dueDate, tag) } } private fun showBeforeDueDateReminderDialog( context: Context, dueDate: Date, - @ColorInt color: Int, + @ColorInt color: Int ) = callbackFlow { val choices = listOf( ReminderChoice.Minute(5), @@ -115,10 +116,11 @@ class ReminderManager( contentId: Long, contentName: String, contentHtmlUrl: String, - dueDate: Date? + dueDate: Date?, + tag: String? = null ) { showCustomReminderDialog(context).collect { calendar -> - createReminder(context, calendar, userId, contentId, contentName, contentHtmlUrl, dueDate) + createReminder(context, calendar, userId, contentId, contentName, contentHtmlUrl, dueDate, tag) } } @@ -185,7 +187,8 @@ class ReminderManager( contentId: Long, contentName: String, contentHtmlUrl: String, - dueDate: Date? + dueDate: Date?, + tag: String? = null ) { val alarmTimeInMillis = calendar.timeInMillis if (reminderRepository.isReminderAlreadySetForTime(userId, contentId, calendar.timeInMillis)) { @@ -220,7 +223,8 @@ class ReminderManager( contentHtmlUrl, reminderTitle, reminderMessage, - alarmTimeInMillis + alarmTimeInMillis, + tag ) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderRepository.kt index f626810df0..c44ba84457 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderRepository.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderRepository.kt @@ -32,7 +32,8 @@ class ReminderRepository( contentHtmlUrl: String, title: String, alarmText: String, - alarmTimeInMillis: Long + alarmTimeInMillis: Long, + tag: String? = null ) { val reminder = ReminderEntity( userId = userId, @@ -40,7 +41,8 @@ class ReminderRepository( name = title, htmlUrl = contentHtmlUrl, text = Date(alarmTimeInMillis).toFormattedString(), - time = alarmTimeInMillis + time = alarmTimeInMillis, + tag = tag ) val reminderId = reminderDao.insert(reminder) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderViewState.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderViewState.kt index ff311088c8..ef346fa174 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderViewState.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/ReminderViewState.kt @@ -9,6 +9,8 @@ data class ReminderViewState( val reminders: List = emptyList(), val dueDate: Date? = null, val themeColor: Color? = null, + val dueLabel: String? = null, + val tag: String? = null ) { fun getThemeColor(context: Context): Color { return themeColor ?: Color(context.getColor(R.color.textDarkest)) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/composables/ReminderView.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/composables/ReminderView.kt index 3a90036497..b32df8d3b7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/composables/ReminderView.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/reminder/composables/ReminderView.kt @@ -51,12 +51,13 @@ import com.instructure.pandautils.utils.toFormattedString @Composable fun ReminderView( viewState: ReminderViewState, - onAddClick: () -> Unit, + modifier: Modifier = Modifier, + onAddClick: (String?) -> Unit, onRemoveClick: (Long) -> Unit, ) { CanvasTheme { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(vertical = 24.dp, horizontal = 16.dp) ) { @@ -71,9 +72,11 @@ fun ReminderView( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .padding(vertical = 12.dp) - .clickable { onAddClick() } + .clickable { + onAddClick(viewState.tag) + } ) { - IconButton(onClick = { onAddClick() }) { + IconButton(onClick = { onAddClick(viewState.tag) }) { Icon( painter = painterResource(id = R.drawable.ic_add), contentDescription = stringResource(id = R.string.a11y_addReminder), diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt index 7b0e9b34fa..0f75d10ff9 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt @@ -44,7 +44,7 @@ import com.instructure.pandautils.room.common.Converters ModuleBulkProgressEntity::class, AssignmentListSelectedFiltersEntity::class, FileDownloadProgressEntity::class - ], version = 12 + ], version = 13 ) @TypeConverters(Converters::class, AssignmentFilterConverter::class) abstract class AppDatabase : RoomDatabase() { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabaseMigrations.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabaseMigrations.kt index b5f427495b..bcdd9e678b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabaseMigrations.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabaseMigrations.kt @@ -69,5 +69,9 @@ val appDatabaseMigrations = arrayOf( createMigration(11, 12) { database -> database.execSQL("CREATE TABLE IF NOT EXISTS FileDownloadProgressEntity (workerId TEXT NOT NULL, fileName TEXT NOT NULL, progress INTEGER NOT NULL, progressState TEXT NOT NULL, filePath TEXT NOT NULL, PRIMARY KEY(workerId))") - } + }, + + createMigration(12, 13) { database -> + database.execSQL("ALTER TABLE ReminderEntity ADD COLUMN tag TEXT") + }, ) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/ReminderEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/ReminderEntity.kt index e98f494de8..ed14028908 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/ReminderEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/ReminderEntity.kt @@ -30,5 +30,6 @@ data class ReminderEntity( val htmlUrl: String, val name: String, val text: String, - val time: Long + val time: Long, + val tag: String? = null ) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt index fbc3704cd9..ff10789041 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt @@ -29,6 +29,8 @@ import com.instructure.pandautils.room.offline.daos.AssignmentScoreStatisticsDao import com.instructure.pandautils.room.offline.daos.AssignmentSetDao import com.instructure.pandautils.room.offline.daos.AttachmentDao import com.instructure.pandautils.room.offline.daos.AuthorDao +import com.instructure.pandautils.room.offline.daos.CheckpointDao +import com.instructure.pandautils.room.offline.daos.SubAssignmentSubmissionDao import com.instructure.pandautils.room.offline.daos.ConferenceDao import com.instructure.pandautils.room.offline.daos.ConferenceRecodingDao import com.instructure.pandautils.room.offline.daos.CourseDao @@ -93,7 +95,9 @@ import com.instructure.pandautils.room.offline.entities.AssignmentScoreStatistic import com.instructure.pandautils.room.offline.entities.AssignmentSetEntity import com.instructure.pandautils.room.offline.entities.AttachmentEntity import com.instructure.pandautils.room.offline.entities.AuthorEntity +import com.instructure.pandautils.room.offline.entities.CheckpointEntity import com.instructure.pandautils.room.offline.entities.ConferenceEntity +import com.instructure.pandautils.room.offline.entities.SubAssignmentSubmissionEntity import com.instructure.pandautils.room.offline.entities.ConferenceRecordingEntity import com.instructure.pandautils.room.offline.entities.CourseEntity import com.instructure.pandautils.room.offline.entities.CourseFeaturesEntity @@ -226,8 +230,10 @@ import com.instructure.pandautils.room.offline.entities.UserEntity CourseSyncProgressEntity::class, FileSyncProgressEntity::class, StudioMediaProgressEntity::class, - CustomGradeStatusEntity::class - ], version = 5 + CustomGradeStatusEntity::class, + CheckpointEntity::class, + SubAssignmentSubmissionEntity::class + ], version = 6 ) @TypeConverters(value = [Converters::class, OfflineConverters::class]) abstract class OfflineDatabase : RoomDatabase() { @@ -357,4 +363,8 @@ abstract class OfflineDatabase : RoomDatabase() { abstract fun studioMediaProgressDao(): StudioMediaProgressDao abstract fun customGradeStatusDao(): CustomGradeStatusDao + + abstract fun checkpointDao(): CheckpointDao + + abstract fun subAssignmentSubmissionDao(): SubAssignmentSubmissionDao } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt index baccad242f..5b09de227b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt @@ -119,5 +119,41 @@ val offlineDatabaseMigrations = arrayOf( "PRIMARY KEY(`id`, `courseId`)," + "FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)" ) + }, + createMigration(5, 6) { database -> + database.execSQL("ALTER TABLE `SubmissionEntity` ADD COLUMN `hasSubAssignmentSubmissions` INTEGER NOT NULL DEFAULT 0") + database.execSQL("ALTER TABLE `DiscussionTopicHeaderEntity` ADD COLUMN `replyRequiredCount` INTEGER") + database.execSQL( + "CREATE TABLE IF NOT EXISTS `CheckpointEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + + "`assignmentId` INTEGER NOT NULL," + + "`name` TEXT," + + "`tag` TEXT," + + "`pointsPossible` REAL," + + "`dueAt` TEXT," + + "`onlyVisibleToOverrides` INTEGER NOT NULL," + + "`lockAt` TEXT," + + "`unlockAt` TEXT," + + "FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)" + ) + database.execSQL( + "CREATE TABLE IF NOT EXISTS `SubAssignmentSubmissionEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + + "`submissionId` INTEGER NOT NULL," + + "`submissionAttempt` INTEGER NOT NULL," + + "`grade` TEXT," + + "`score` REAL NOT NULL," + + "`late` INTEGER NOT NULL," + + "`excused` INTEGER NOT NULL," + + "`missing` INTEGER NOT NULL," + + "`latePolicyStatus` TEXT," + + "`customGradeStatusId` INTEGER," + + "`subAssignmentTag` TEXT," + + "`enteredScore` REAL NOT NULL," + + "`enteredGrade` TEXT," + + "`userId` INTEGER NOT NULL," + + "`isGradeMatchesCurrentSubmission` INTEGER NOT NULL," + + "FOREIGN KEY(`submissionId`, `submissionAttempt`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE)" + ) } ) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt new file mode 100644 index 0000000000..5ed1e092de --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 - 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.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.instructure.pandautils.room.offline.entities.CheckpointEntity + +@Dao +interface CheckpointDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: CheckpointEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Query("SELECT * FROM CheckpointEntity WHERE assignmentId = :assignmentId") + suspend fun findByAssignmentId(assignmentId: Long): List + + @Query("DELETE FROM CheckpointEntity WHERE assignmentId = :assignmentId") + suspend fun deleteByAssignmentId(assignmentId: Long) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDao.kt new file mode 100644 index 0000000000..f3c6870a5d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 - 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.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.instructure.pandautils.room.offline.entities.SubAssignmentSubmissionEntity + +@Dao +interface SubAssignmentSubmissionDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: SubAssignmentSubmissionEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Query("SELECT * FROM SubAssignmentSubmissionEntity WHERE submissionId = :submissionId AND submissionAttempt = :submissionAttempt") + suspend fun findBySubmissionIdAndAttempt(submissionId: Long, submissionAttempt: Long): List + + @Query("DELETE FROM SubAssignmentSubmissionEntity WHERE submissionId = :submissionId AND submissionAttempt = :submissionAttempt") + suspend fun deleteBySubmissionIdAndAttempt(submissionId: Long, submissionAttempt: Long) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentEntity.kt index 5cb18f0e4a..ca43674b85 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentEntity.kt @@ -134,7 +134,8 @@ data class AssignmentEntity( lockInfo: LockInfo? = null, discussionTopicHeader: DiscussionTopicHeader? = null, scoreStatistics: AssignmentScoreStatistics? = null, - plannerOverride: PlannerOverride? = null + plannerOverride: PlannerOverride? = null, + checkpoints: List = emptyList() ) = Assignment( id = id, name = name, @@ -186,6 +187,7 @@ data class AssignmentEntity( inClosedGradingPeriod = inClosedGradingPeriod, annotatableAttachmentId = annotatableAttachmentId, anonymousSubmissions = anonymousSubmissions, - omitFromFinalGrade = omitFromFinalGrade + omitFromFinalGrade = omitFromFinalGrade, + checkpoints = checkpoints ) } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt new file mode 100644 index 0000000000..52ea48ba73 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2025 - 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.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Checkpoint + +@Entity( + foreignKeys = [ + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class CheckpointEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val assignmentId: Long, + val name: String?, + val tag: String?, + val pointsPossible: Double?, + val dueAt: String?, + val onlyVisibleToOverrides: Boolean, + val lockAt: String?, + val unlockAt: String? +) { + constructor(checkpoint: Checkpoint, assignmentId: Long) : this( + assignmentId = assignmentId, + name = checkpoint.name, + tag = checkpoint.tag, + pointsPossible = checkpoint.pointsPossible, + dueAt = checkpoint.dueAt, + onlyVisibleToOverrides = checkpoint.onlyVisibleToOverrides, + lockAt = checkpoint.lockAt, + unlockAt = checkpoint.unlockAt + ) + + fun toApiModel() = Checkpoint( + name = name, + tag = tag, + pointsPossible = pointsPossible, + dueAt = dueAt, + overrides = null, + onlyVisibleToOverrides = onlyVisibleToOverrides, + lockAt = lockAt, + unlockAt = unlockAt + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicHeaderEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicHeaderEntity.kt index 4b26468d14..7e551cc76b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicHeaderEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicHeaderEntity.kt @@ -86,7 +86,8 @@ data class DiscussionTopicHeaderEntity( var lockAt: Date?, var userCanSeePosts: Boolean, var specificSections: String?, - var anonymousState: String? + var anonymousState: String?, + var replyRequiredCount: Int? ) { constructor(discussionTopicHeader: DiscussionTopicHeader, courseId: Long, permissionId: Long? = null) : this( discussionTopicHeader.id, @@ -121,7 +122,8 @@ data class DiscussionTopicHeaderEntity( discussionTopicHeader.lockAt, discussionTopicHeader.userCanSeePosts, discussionTopicHeader.specificSections, - discussionTopicHeader.anonymousState + discussionTopicHeader.anonymousState, + discussionTopicHeader.replyRequiredCount ) fun toApiModel( @@ -170,5 +172,6 @@ data class DiscussionTopicHeaderEntity( //TODO sections = null, anonymousState = anonymousState, + replyRequiredCount = replyRequiredCount ) } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubAssignmentSubmissionEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubAssignmentSubmissionEntity.kt new file mode 100644 index 0000000000..cb40b005d0 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubAssignmentSubmissionEntity.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2025 - 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.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.SubAssignmentSubmission + +@Entity( + foreignKeys = [ + ForeignKey( + entity = SubmissionEntity::class, + parentColumns = ["id", "attempt"], + childColumns = ["submissionId", "submissionAttempt"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class SubAssignmentSubmissionEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val submissionId: Long, + val submissionAttempt: Long, + val grade: String?, + val score: Double, + val late: Boolean, + val excused: Boolean, + val missing: Boolean, + val latePolicyStatus: String?, + val customGradeStatusId: Long?, + val subAssignmentTag: String?, + val enteredScore: Double, + val enteredGrade: String?, + val userId: Long, + val isGradeMatchesCurrentSubmission: Boolean +) { + constructor(subAssignmentSubmission: SubAssignmentSubmission, submissionId: Long, submissionAttempt: Long) : this( + submissionId = submissionId, + submissionAttempt = submissionAttempt, + grade = subAssignmentSubmission.grade, + score = subAssignmentSubmission.score, + late = subAssignmentSubmission.late, + excused = subAssignmentSubmission.excused, + missing = subAssignmentSubmission.missing, + latePolicyStatus = subAssignmentSubmission.latePolicyStatus, + customGradeStatusId = subAssignmentSubmission.customGradeStatusId, + subAssignmentTag = subAssignmentSubmission.subAssignmentTag, + enteredScore = subAssignmentSubmission.enteredScore, + enteredGrade = subAssignmentSubmission.enteredGrade, + userId = subAssignmentSubmission.userId, + isGradeMatchesCurrentSubmission = subAssignmentSubmission.isGradeMatchesCurrentSubmission + ) + + fun toApiModel() = SubAssignmentSubmission( + grade = grade, + score = score, + late = late, + excused = excused, + missing = missing, + latePolicyStatus = latePolicyStatus, + customGradeStatusId = customGradeStatusId, + subAssignmentTag = subAssignmentTag, + enteredScore = enteredScore, + enteredGrade = enteredGrade, + userId = userId, + isGradeMatchesCurrentSubmission = isGradeMatchesCurrentSubmission + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionEntity.kt index 2467a887c2..ed2faa4092 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionEntity.kt @@ -24,6 +24,7 @@ import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.MediaComment import com.instructure.canvasapi2.models.RubricCriterionAssessment +import com.instructure.canvasapi2.models.SubAssignmentSubmission import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.SubmissionComment import com.instructure.canvasapi2.models.User @@ -83,7 +84,8 @@ data class SubmissionEntity( val enteredGrade: String?, val postedAt: Date?, val gradingPeriodId: Long?, - val customGradeStatusId: Long? + val customGradeStatusId: Long?, + val hasSubAssignmentSubmissions: Boolean ) { constructor(submission: Submission, groupId: Long?, mediaCommentId: String?) : this( id = submission.id, @@ -114,7 +116,8 @@ data class SubmissionEntity( enteredGrade = submission.enteredGrade, postedAt = submission.postedAt, gradingPeriodId = submission.gradingPeriodId, - customGradeStatusId = submission.customGradeStatusId + customGradeStatusId = submission.customGradeStatusId, + hasSubAssignmentSubmissions = submission.hasSubAssignmentSubmissions ) fun toApiModel( @@ -125,7 +128,8 @@ data class SubmissionEntity( mediaComment: MediaComment? = null, assignment: Assignment? = null, user: User? = null, - group: Group? = null + group: Group? = null, + subAssignmentSubmissions: ArrayList = arrayListOf() ) = Submission( id = id, grade = grade, @@ -163,6 +167,8 @@ data class SubmissionEntity( enteredGrade = enteredGrade, postedAt = postedAt, gradingPeriodId = gradingPeriodId, - customGradeStatusId = customGradeStatusId + customGradeStatusId = customGradeStatusId, + hasSubAssignmentSubmissions = hasSubAssignmentSubmissions, + subAssignmentSubmissions = subAssignmentSubmissions ) } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/AssignmentFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/AssignmentFacade.kt index 888ac63d26..9b8ddeb8a7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/AssignmentFacade.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/AssignmentFacade.kt @@ -37,6 +37,7 @@ class AssignmentFacade( private val lockInfoFacade: LockInfoFacade, private val rubricCriterionRatingDao: RubricCriterionRatingDao, private val assignmentRubricCriterionDao: AssignmentRubricCriterionDao, + private val checkpointDao: CheckpointDao, private val offlineDatabase: OfflineDatabase ) { @@ -94,6 +95,10 @@ class AssignmentFacade( assignment.lockInfo?.let { lockInfoFacade.insertLockInfoForAssignment(it, assignment.id) } + + checkpointDao.insertAll(assignment.checkpoints.map { + CheckpointEntity(it, assignment.id) + }) } private suspend fun insertPlannerOverride(plannerOverride: PlannerOverride?): Long? { @@ -132,6 +137,7 @@ class AssignmentFacade( val rubricCriterionEntities = assignmentRubricCriterionDao.findByAssignmentId(assignmentEntity.id).mapNotNull { rubricCriterionDao.findById(it.rubricId) } + val checkpointEntities = checkpointDao.findByAssignmentId(assignmentEntity.id) return assignmentEntity.toApiModel( rubric = rubricCriterionEntities.map { rubricCriterionEntity -> @@ -143,7 +149,8 @@ class AssignmentFacade( lockInfo = lockInfo, discussionTopicHeader = discussionTopicHeader, scoreStatistics = scoreStatisticsEntity?.toApiModel(), - plannerOverride = plannerOverrideEntity?.toApiModel() + plannerOverride = plannerOverrideEntity?.toApiModel(), + checkpoints = checkpointEntities.map { it.toApiModel() } ).apply { /* * the assignment model has a submission that contains the assignment, but the inner assignment model cannot diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SubmissionFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SubmissionFacade.kt index 971e76e94c..a946de391a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SubmissionFacade.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SubmissionFacade.kt @@ -32,7 +32,8 @@ class SubmissionFacade( private val submissionCommentDao: SubmissionCommentDao, private val attachmentDao: AttachmentDao, private val authorDao: AuthorDao, - private val rubricCriterionAssessmentDao: RubricCriterionAssessmentDao + private val rubricCriterionAssessmentDao: RubricCriterionAssessmentDao, + private val subAssignmentSubmissionDao: SubAssignmentSubmissionDao ) { suspend fun insertSubmission(submission: Submission) { @@ -75,6 +76,10 @@ class SubmissionFacade( submission.submissionHistory.forEach { submissionHistoryItem -> submissionHistoryItem?.let { insertSubmission(it) } } + + subAssignmentSubmissionDao.insertAll(submission.subAssignmentSubmissions.map { + SubAssignmentSubmissionEntity(it, submission.id, submission.attempt) + }) } suspend fun getSubmissionById(id: Long): Submission? { @@ -93,6 +98,7 @@ class SubmissionFacade( val submissionCommentEntities = submissionCommentDao.findBySubmissionId(submissionEntity.id) val attachmentEntities = attachmentDao.findBySubmissionId(submissionEntity.id) val rubricCriterionAssessmentEntities = rubricCriterionAssessmentDao.findByAssignmentId(submissionEntity.assignmentId) + val subAssignmentSubmissionEntities = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(submissionEntity.id, submissionEntity.attempt) return submissionEntity.toApiModel( mediaComment = mediaCommentEntity?.toApiModel(), @@ -100,7 +106,8 @@ class SubmissionFacade( group = groupEntity?.toApiModel(), submissionComments = submissionCommentEntities.map { it.toApiModel() }, attachments = attachmentEntities.filter { it.attempt == submissionEntity.attempt }.map { it.toApiModel() }, - rubricAssessment = HashMap(rubricCriterionAssessmentEntities.associateBy({ it.id }, { it.toApiModel() })) + rubricAssessment = HashMap(rubricCriterionAssessmentEntities.associateBy({ it.id }, { it.toApiModel() })), + subAssignmentSubmissions = ArrayList(subAssignmentSubmissionEntities.map { it.toApiModel() }) ) } diff --git a/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml b/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml index e7b42ce6cf..e15e3e0b33 100644 --- a/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml +++ b/libs/pandautils/src/main/res/layout/fragment_assignment_details.xml @@ -268,7 +268,7 @@ android:visibility="@{viewModel.data.fullLocked ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/reminderBottomDivider" /> + app:layout_constraintTop_toBottomOf="@id/dueComposeView" /> - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/dueDivider" /> + app:layout_constraintTop_toBottomOf="@id/dueComposeView" /> "${(call.invocation.args[1] as Array<*>)[0]} Before" } + + val course = + Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val checkpoint1 = Checkpoint( + tag = "reply_to_topic", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString() + ) + val checkpoint2 = Checkpoint( + tag = "reply_to_entry", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 2) }.time.toApiString() + ) + + val subSubmission1 = SubAssignmentSubmission( + subAssignmentTag = "reply_to_topic", + + ) + val subSubmission2 = SubAssignmentSubmission(subAssignmentTag = "reply_to_entry") + + val assignment = Assignment( + checkpoints = listOf(checkpoint1, checkpoint2), + submission = Submission( + subAssignmentSubmissions = arrayListOf(subSubmission1, subSubmission2) + ) + ) + coEvery { + assignmentDetailsRepository.getAssignment( + any(), + any(), + any(), + any() + ) + } returns assignment + + val viewModel = getViewModel(realReminderManager) + + assertEquals( + reminderEntities.filter { it.tag == "reply_to_topic" }.map { it.id }, + viewModel.dueDateReminderViewStates[0].reminders.map { it.id } + ) + + assertEquals( + reminderEntities.filter { it.tag == "reply_to_entry" }.map { it.id }, + viewModel.dueDateReminderViewStates[1].reminders.map { it.id } ) } @@ -823,11 +889,80 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel(realReminderManager) - assertEquals(0, viewModel.data.value?.reminders?.size) + assertEquals(0, viewModel.dueDateReminderViewStates[0].reminders.size) remindersLiveData.value = listOf(ReminderEntity(1, 1, 1, "htmlUrl1", "Assignment 1", "1 day", 1000)) - assertEquals(ReminderViewData(1, "1 day"), viewModel.data.value?.reminders?.first()?.data) + assertEquals( + listOf(1L), + viewModel.dueDateReminderViewStates[0].reminders.map { it.id } + ) + } + + @Test + fun `Reminders update correctly for discussion checkpoints`() { + val remindersLiveData = MutableLiveData>() + val dateTimePicker: DateTimePicker = mockk(relaxed = true) + val reminderRepository: ReminderRepository = mockk(relaxed = true) + val realReminderManager = ReminderManager(dateTimePicker, reminderRepository, analytics) + every { reminderRepository.findByAssignmentIdLiveData(any(), any()) } returns remindersLiveData + every { resources.getString(eq(R.string.reminderBefore), any()) } answers { call -> "${(call.invocation.args[1] as Array<*>)[0]} Before" } + + val course = + Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val checkpoint1 = Checkpoint( + tag = "reply_to_topic", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString() + ) + val checkpoint2 = Checkpoint( + tag = "reply_to_entry", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 2) }.time.toApiString() + ) + + val subSubmission1 = SubAssignmentSubmission( + subAssignmentTag = "reply_to_topic", + + ) + val subSubmission2 = SubAssignmentSubmission(subAssignmentTag = "reply_to_entry") + + val assignment = Assignment( + checkpoints = listOf(checkpoint1, checkpoint2), + submission = Submission( + subAssignmentSubmissions = arrayListOf(subSubmission1, subSubmission2) + ) + ) + coEvery { + assignmentDetailsRepository.getAssignment( + any(), + any(), + any(), + any() + ) + } returns assignment + + val viewModel = getViewModel(realReminderManager) + + assertEquals(0, viewModel.dueDateReminderViewStates[0].reminders.size) + assertEquals(0, viewModel.dueDateReminderViewStates[1].reminders.size) + + remindersLiveData.value = listOf( + ReminderEntity(1, 1, 1, "htmlUrl1", "Assignment 1", "1 day", 1000, "reply_to_topic"), + ReminderEntity(2, 1, 1, "htmlUrl1", "Assignment 1", "2 day", 2000, "reply_to_entry") + ) + + assertEquals( + listOf(1L), + viewModel.dueDateReminderViewStates[0].reminders.map { it.id } + ) + + assertEquals( + listOf(2L), + viewModel.dueDateReminderViewStates[1].reminders.map { it.id } + ) } @Test @@ -1121,4 +1256,53 @@ class AssignmentDetailsViewModelTest { assertFalse(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToSubmissionScreen) } + + @Test + fun `Assignment with checkpoints and subAssignmentSubmissions maps dueDateReminderViewStates correctly`() { + val course = + Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val checkpoint1 = Checkpoint( + tag = "reply_to_topic", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString() + ) + val checkpoint2 = Checkpoint( + tag = "reply_to_entry", + dueAt = Calendar.getInstance() + .apply { add(Calendar.DAY_OF_MONTH, 2) }.time.toApiString() + ) + + val subSubmission1 = SubAssignmentSubmission( + subAssignmentTag = "reply_to_topic", + + ) + val subSubmission2 = SubAssignmentSubmission(subAssignmentTag = "reply_to_entry") + + + val assignment = Assignment( + checkpoints = listOf(checkpoint1, checkpoint2), + submission = Submission( + subAssignmentSubmissions = arrayListOf(subSubmission1, subSubmission2) + ) + ) + coEvery { + assignmentDetailsRepository.getAssignment( + any(), + any(), + any(), + any() + ) + } returns assignment + + val viewModel = getViewModel() + + assertEquals(2, viewModel.dueDateReminderViewStates.size) + assertEquals("reply_to_topic", viewModel.dueDateReminderViewStates[0].tag) + assertEquals("reply_to_entry", viewModel.dueDateReminderViewStates[1].tag) + + assertTrue(viewModel.dueDateReminderViewStates[0].reminders.isEmpty()) + assertTrue(viewModel.dueDateReminderViewStates[1].reminders.isEmpty()) + } } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/AssignmentFacadeTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/AssignmentFacadeTest.kt index cf3de7f4c8..79d099d9a6 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/AssignmentFacadeTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/AssignmentFacadeTest.kt @@ -21,6 +21,7 @@ import androidx.room.withTransaction import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.AssignmentGroup import com.instructure.canvasapi2.models.AssignmentScoreStatistics +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.LockInfo import com.instructure.canvasapi2.models.PlannableType @@ -33,6 +34,7 @@ import com.instructure.pandautils.room.offline.daos.AssignmentDao import com.instructure.pandautils.room.offline.daos.AssignmentGroupDao import com.instructure.pandautils.room.offline.daos.AssignmentRubricCriterionDao import com.instructure.pandautils.room.offline.daos.AssignmentScoreStatisticsDao +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.PlannerOverrideDao import com.instructure.pandautils.room.offline.daos.RubricCriterionDao import com.instructure.pandautils.room.offline.daos.RubricCriterionRatingDao @@ -41,6 +43,7 @@ import com.instructure.pandautils.room.offline.entities.AssignmentEntity import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity import com.instructure.pandautils.room.offline.entities.AssignmentRubricCriterionEntity import com.instructure.pandautils.room.offline.entities.AssignmentScoreStatisticsEntity +import com.instructure.pandautils.room.offline.entities.CheckpointEntity import com.instructure.pandautils.room.offline.entities.PlannerOverrideEntity import com.instructure.pandautils.room.offline.entities.RubricCriterionEntity import com.instructure.pandautils.room.offline.entities.RubricSettingsEntity @@ -73,6 +76,7 @@ class AssignmentFacadeTest { private val lockInfoFacade: LockInfoFacade = mockk(relaxed = true) private val rubricCriterionRatingDao: RubricCriterionRatingDao = mockk(relaxed = true) private val assignmentRubricCriterionDao: AssignmentRubricCriterionDao = mockk(relaxed = true) + private val checkpointDao: CheckpointDao = mockk(relaxed = true) private val offlineDatabase: OfflineDatabase = mockk(relaxed = true) private val facade = AssignmentFacade( @@ -87,6 +91,7 @@ class AssignmentFacadeTest { lockInfoFacade, rubricCriterionRatingDao, assignmentRubricCriterionDao, + checkpointDao, offlineDatabase ) @@ -118,6 +123,7 @@ class AssignmentFacadeTest { val scoreStatistics = AssignmentScoreStatistics(0.0, 0.0, 0.0) val rubricCriterions = listOf(RubricCriterion()) val lockInfo = LockInfo() + val checkpoints = listOf(Checkpoint(name = "Checkpoint 1", tag = "checkpoint_1")) val assignments = listOf( Assignment( rubricSettings = rubricSettings, @@ -128,6 +134,7 @@ class AssignmentFacadeTest { rubric = rubricCriterions, lockInfo = lockInfo, courseId = 1, + checkpoints = checkpoints ) ) val assignmentGroups = listOf(AssignmentGroup(assignments = assignments)) @@ -141,6 +148,7 @@ class AssignmentFacadeTest { coEvery { assignmentScoreStatisticsDao.insert(any()) } just Runs coEvery { rubricCriterionDao.insert(any()) } just Runs coEvery { lockInfoFacade.insertLockInfoForAssignment(any(), any()) } just Runs + coEvery { checkpointDao.insertAll(any()) } just Runs facade.insertAssignmentGroups(assignmentGroups, 1L) @@ -157,6 +165,7 @@ class AssignmentFacadeTest { coVerify { assignmentRubricCriterionDao.insert(AssignmentRubricCriterionEntity(assignment.id, it.id.orEmpty())) } } coVerify { lockInfoFacade.insertLockInfoForAssignment(lockInfo, assignment.id) } + coVerify { checkpointDao.insertAll(checkpoints.map { CheckpointEntity(it, assignment.id) }) } coVerify { assignmentDao.insertOrUpdate( AssignmentEntity( @@ -181,6 +190,7 @@ class AssignmentFacadeTest { val scoreStatistics = AssignmentScoreStatistics(0.0, 0.0, 0.0) val rubricCriterions = listOf(RubricCriterion()) val lockInfo = LockInfo() + val checkpoints = listOf(Checkpoint(name = "Checkpoint 1", tag = "checkpoint_1")) val assignment = Assignment( rubricSettings = rubricSettings, submission = submission, @@ -190,6 +200,7 @@ class AssignmentFacadeTest { rubric = rubricCriterions, lockInfo = lockInfo, courseId = 1, + checkpoints = checkpoints ) coEvery { assignmentDao.insert(any()) } just Runs @@ -200,6 +211,7 @@ class AssignmentFacadeTest { coEvery { assignmentScoreStatisticsDao.insert(any()) } just Runs coEvery { rubricCriterionDao.insert(any()) } just Runs coEvery { lockInfoFacade.insertLockInfoForAssignment(any(), any()) } just Runs + coEvery { checkpointDao.insertAll(any()) } just Runs facade.insertAssignment(assignment) @@ -219,6 +231,7 @@ class AssignmentFacadeTest { coVerify { rubricCriterionDao.insert(RubricCriterionEntity(it, assignment.id)) } } coVerify { lockInfoFacade.insertLockInfoForAssignment(lockInfo, assignment.id) } + coVerify { checkpointDao.insertAll(checkpoints.map { CheckpointEntity(it, assignment.id) }) } coVerify { assignmentDao.insertOrUpdate( AssignmentEntity( diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/SubmissionFacadeTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/SubmissionFacadeTest.kt index de305395b4..e853245644 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/SubmissionFacadeTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/SubmissionFacadeTest.kt @@ -26,6 +26,7 @@ import com.instructure.pandautils.room.offline.daos.AuthorDao import com.instructure.pandautils.room.offline.daos.GroupDao import com.instructure.pandautils.room.offline.daos.MediaCommentDao import com.instructure.pandautils.room.offline.daos.RubricCriterionAssessmentDao +import com.instructure.pandautils.room.offline.daos.SubAssignmentSubmissionDao import com.instructure.pandautils.room.offline.daos.SubmissionCommentDao import com.instructure.pandautils.room.offline.daos.SubmissionDao import com.instructure.pandautils.room.offline.daos.UserDao @@ -33,8 +34,10 @@ import com.instructure.pandautils.room.offline.entities.GroupEntity import com.instructure.pandautils.room.offline.entities.MediaCommentEntity import com.instructure.pandautils.room.offline.entities.SubmissionEntity import com.instructure.pandautils.room.offline.entities.UserEntity +import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.just import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Assert @@ -50,10 +53,11 @@ class SubmissionFacadeTest { private val attachmentDao: AttachmentDao = mockk(relaxed = true) private val authorDao: AuthorDao = mockk(relaxed = true) private val rubricCriterionAssessmentDao: RubricCriterionAssessmentDao = mockk(relaxed = true) + private val subAssignmentSubmissionDao: SubAssignmentSubmissionDao = mockk(relaxed = true) private val facade = SubmissionFacade( submissionDao, groupDao, mediaCommentDao, userDao, - submissionCommentDao, attachmentDao, authorDao, rubricCriterionAssessmentDao + submissionCommentDao, attachmentDao, authorDao, rubricCriterionAssessmentDao, subAssignmentSubmissionDao ) @Test @@ -73,6 +77,7 @@ class SubmissionFacadeTest { ) coEvery { submissionDao.insert(any()) } returns 1L + coEvery { subAssignmentSubmissionDao.insertAll(any()) } just Runs facade.insertSubmission(submission) @@ -80,6 +85,7 @@ class SubmissionFacadeTest { coVerify { mediaCommentDao.insert(MediaCommentEntity(mediaComment, 1L, 0)) } coVerify { userDao.insertOrUpdate(UserEntity(user)) } coVerify { submissionDao.insertOrUpdate(SubmissionEntity(submission, group.id, mediaComment.mediaId)) } + coVerify { subAssignmentSubmissionDao.insertAll(emptyList()) } } @Test @@ -95,6 +101,7 @@ class SubmissionFacadeTest { coEvery { mediaCommentDao.findById(any()) } returns MediaCommentEntity(mediaComment, 1L, 0) coEvery { userDao.findById(any()) } returns UserEntity(user) coEvery { submissionDao.findById(any()) } returns submissionHistory.map { SubmissionEntity(it, group.id, mediaComment.mediaId) } + coEvery { subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(any(), any()) } returns emptyList() val result = facade.getSubmissionById(submissionId)!! @@ -134,6 +141,7 @@ class SubmissionFacadeTest { ) } coEvery { submissionDao.findById(submissionId) } returns submissionHistory.map { SubmissionEntity(it, group.id, mediaComment.mediaId) } + coEvery { subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(any(), any()) } returns emptyList() val result = facade.findByAssignmentIds(listOf(assignmentId)) From 190557f3217a8e86530af713c9e0de3660b0767c Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Mon, 13 Oct 2025 12:45:54 +0200 Subject: [PATCH 38/94] [MBL-19275][Android] Fix file attachment duplicate uploads and infinite loading (#3295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix race condition causing duplicate file uploads and infinite loading indicators in SpeedGrader comments by passing file paths atomically with upload events. refs: MBL-19275 affects: Teacher release note: Fixed an issue where file attachments in SpeedGrader comments could be uploaded multiple times and get stuck in an infinite loading state test plan: - Unit tests: All 20 tests pass including 5 new tests for file upload flows - Manual testing: Build and run Teacher app on emulator - Verify file uploads in SpeedGrader comments work correctly without duplicates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- .../file/upload/FileUploadDialogFragment.kt | 3 +- .../file/upload/FileUploadEventHandler.kt | 3 +- .../comments/SpeedGraderCommentsSection.kt | 22 +- .../comments/SpeedGraderCommentsViewModel.kt | 18 +- .../SpeedGraderCommentsViewModelTest.kt | 198 ++++++++++++++++++ 5 files changed, 224 insertions(+), 20 deletions(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt index 35af8e5318..9a835b82fd 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt @@ -256,7 +256,8 @@ class FileUploadDialogFragment : BaseCanvasDialogFragment() { fileUploadEventHandler.postEvent( FileUploadEvent.UploadStarted( action.id, - action.liveData + action.liveData, + action.selectedUris ) ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt index 5c18ce2721..225cb4f9fc 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt @@ -29,7 +29,8 @@ sealed class FileUploadEvent { data class FileSelected(val filePaths: List) : FileUploadEvent() data class UploadStarted( val uuid: UUID?, - val workInfoLiveData: LiveData + val workInfoLiveData: LiveData, + val filePaths: List ) : FileUploadEvent() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsSection.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsSection.kt index 662e634287..4c8f40b2ef 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsSection.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsSection.kt @@ -208,16 +208,20 @@ fun SpeedGraderCommentsSection( if (state.fileSelectorDialogData != null) { val fragmentManager = LocalContext.current.getFragmentActivity().supportFragmentManager - val bundle = FileUploadDialogFragment.createTeacherSubmissionCommentBundle( - state.fileSelectorDialogData.courseId, - state.fileSelectorDialogData.assignmentId, - state.fileSelectorDialogData.userId, - state.fileSelectorDialogData.attempt - ) + // Check if dialog is already showing to prevent duplicates + val existingDialog = fragmentManager.findFragmentByTag(FileUploadDialogFragment.TAG) + if (existingDialog == null) { + val bundle = FileUploadDialogFragment.createTeacherSubmissionCommentBundle( + state.fileSelectorDialogData.courseId, + state.fileSelectorDialogData.assignmentId, + state.fileSelectorDialogData.userId, + state.fileSelectorDialogData.attempt + ) - FileUploadDialogFragment.newInstance(bundle).show( - fragmentManager, FileUploadDialogFragment.TAG + UUID.randomUUID() - ) + FileUploadDialogFragment.newInstance(bundle).show( + fragmentManager, FileUploadDialogFragment.TAG + ) + } } } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt index 8b20af362f..891080153a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt @@ -206,7 +206,7 @@ class SpeedGraderCommentsViewModel @Inject constructor( fileUploadEventHandler.events.collect { event -> when (event) { is FileUploadEvent.UploadStarted -> { - onFileUploadStarted(event.workInfoLiveData) + onFileUploadStarted(event.workInfoLiveData, event.filePaths) } is FileUploadEvent.FileSelected -> { selectedFilePaths = event.filePaths @@ -347,7 +347,7 @@ class SpeedGraderCommentsViewModel @Inject constructor( } is SpeedGraderCommentsAction.FileUploadStarted -> { - onFileUploadStarted(action.workInfoLiveData) + onFileUploadStarted(action.workInfoLiveData, selectedFilePaths.orEmpty()) } is SpeedGraderCommentsAction.FilesSelected -> { @@ -356,7 +356,7 @@ class SpeedGraderCommentsViewModel @Inject constructor( } } - private fun onFileUploadStarted(workInfoLiveData: LiveData) { + private fun onFileUploadStarted(workInfoLiveData: LiveData, filePaths: List) { _uiState.update { state -> state.copy( fileSelectorDialogData = null, @@ -366,8 +366,8 @@ class SpeedGraderCommentsViewModel @Inject constructor( // Subscribe to the worker's LiveData to observe its state viewModelScope.launch { workInfoLiveData.asFlow().collect { workInfo -> - when (workInfo.state) { - WorkInfo.State.RUNNING -> createPendingFileComment(workInfo) + when (workInfo?.state) { + WorkInfo.State.RUNNING -> createPendingFileComment(workInfo, filePaths) WorkInfo.State.SUCCEEDED -> handleFileUploadSuccess(workInfo) WorkInfo.State.FAILED -> handleFileUploadFailure(workInfo) else -> {} @@ -376,7 +376,7 @@ class SpeedGraderCommentsViewModel @Inject constructor( } } - private suspend fun createPendingFileComment(workInfo: WorkInfo) { + private suspend fun createPendingFileComment(workInfo: WorkInfo, filePaths: List) { var fileUploadInput = fileUploadInputDao.findByWorkerId(workInfo.id.toString()) if (fileUploadInput == null) { fileUploadInput = FileUploadInputEntity( @@ -384,7 +384,7 @@ class SpeedGraderCommentsViewModel @Inject constructor( courseId = courseId, assignmentId = assignmentId, userId = studentId, - filePaths = selectedFilePaths.orEmpty(), + filePaths = filePaths, action = FileUploadWorker.ACTION_TEACHER_SUBMISSION_COMMENT, attemptId = selectedAttemptId.takeIf { assignmentEnhancementsEnabled } ) @@ -399,7 +399,7 @@ class SpeedGraderCommentsViewModel @Inject constructor( this.workerId = workInfo.id this.status = CommentSendStatus.SENDING this.workerInputData = FileUploadWorkerData( - selectedFilePaths.orEmpty(), + filePaths, courseId, assignmentId, studentId @@ -647,7 +647,7 @@ class SpeedGraderCommentsViewModel @Inject constructor( fileUploadInputDao.insert(inputData) WorkManager.getInstance(context).apply { - onFileUploadStarted(getWorkInfoByIdLiveData(worker.id)) + onFileUploadStarted(getWorkInfoByIdLiveData(worker.id), filePaths) enqueue(worker) } } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModelTest.kt index a67d1c17be..2445cda312 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModelTest.kt @@ -17,9 +17,12 @@ package com.instructure.pandautils.features.speedgrader.grade.comments import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle +import androidx.work.WorkInfo import com.instructure.canvasapi2.SubmissionCommentsQuery import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.file.upload.FileUploadEvent import com.instructure.pandautils.features.file.upload.FileUploadEventHandler import com.instructure.pandautils.features.speedgrader.SpeedGraderSelectedAttemptHolder import com.instructure.pandautils.room.appdatabase.daos.AttachmentDao @@ -28,12 +31,15 @@ import com.instructure.pandautils.room.appdatabase.daos.FileUploadInputDao import com.instructure.pandautils.room.appdatabase.daos.MediaCommentDao import com.instructure.pandautils.room.appdatabase.daos.PendingSubmissionCommentDao import com.instructure.pandautils.room.appdatabase.daos.SubmissionCommentDao +import com.instructure.pandautils.room.appdatabase.entities.FileUploadInputEntity import com.instructure.pandautils.room.appdatabase.entities.PendingSubmissionCommentEntity import com.instructure.pandautils.room.appdatabase.model.PendingSubmissionCommentWithFileUploadInput import com.instructure.pandautils.views.RecordingMediaType import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.slot import junit.framework.TestCase.assertEquals import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -41,6 +47,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain @@ -49,6 +56,7 @@ import org.junit.Assert import org.junit.Before import org.junit.Rule import org.junit.Test +import java.util.UUID @ExperimentalCoroutinesApi class SpeedGraderCommentsViewModelTest { @@ -372,4 +380,194 @@ class SpeedGraderCommentsViewModelTest { Thread.sleep(100) assertEquals(0, viewModel.uiState.value.comments.size) } + + @Test + fun `FileUploadEvent UploadStarted uses file paths from event`() = runTest { + val fileUploadEventsFlow = MutableSharedFlow(replay = 1) + coEvery { fileUploadEventHandler.events } returns fileUploadEventsFlow + + val workInfoLiveData = MutableLiveData() + val workInfo = mockk(relaxed = true) + val workerId = UUID.randomUUID() + every { workInfo.id } returns workerId + every { workInfo.state } returns WorkInfo.State.RUNNING + + val expectedFilePaths = listOf("/path/to/file1.pdf", "/path/to/file2.jpg") + val fileUploadInputSlot = slot() + + coEvery { fileUploadInputDao.findByWorkerId(any()) } returns null + coEvery { fileUploadInputDao.insert(capture(fileUploadInputSlot)) } returns Unit + coEvery { pendingSubmissionCommentDao.findByPageId(any()) } returns null + coEvery { pendingSubmissionCommentDao.insert(any()) } returns 1L + + createViewModel() + + // Emit the UploadStarted event with file paths + fileUploadEventsFlow.emit( + FileUploadEvent.UploadStarted( + uuid = workerId, + workInfoLiveData = workInfoLiveData, + filePaths = expectedFilePaths + ) + ) + + advanceUntilIdle() + + // Trigger the worker state change + workInfoLiveData.postValue(workInfo) + + advanceUntilIdle() + + // Verify that FileUploadInputEntity was created with the correct file paths from the event + coVerify { fileUploadInputDao.insert(any()) } + assertEquals(expectedFilePaths, fileUploadInputSlot.captured.filePaths) + } + + @Test + fun `FileUploadEvent UploadStarted creates pending comment with correct file paths`() = runTest { + val fileUploadEventsFlow = MutableSharedFlow(replay = 1) + coEvery { fileUploadEventHandler.events } returns fileUploadEventsFlow + + val workInfoLiveData = MutableLiveData() + val workInfo = mockk(relaxed = true) + val workerId = UUID.randomUUID() + every { workInfo.id } returns workerId + every { workInfo.state } returns WorkInfo.State.RUNNING + + val expectedFilePaths = listOf("/path/to/file1.pdf") + val pendingCommentSlot = slot() + + coEvery { fileUploadInputDao.findByWorkerId(any()) } returns null + coEvery { fileUploadInputDao.insert(any()) } returns Unit + coEvery { pendingSubmissionCommentDao.findByPageId(any()) } returns null + coEvery { pendingSubmissionCommentDao.insert(capture(pendingCommentSlot)) } returns 1L + + createViewModel() + + // Emit the UploadStarted event + fileUploadEventsFlow.emit( + FileUploadEvent.UploadStarted( + uuid = workerId, + workInfoLiveData = workInfoLiveData, + filePaths = expectedFilePaths + ) + ) + + advanceUntilIdle() + + // Trigger worker state + workInfoLiveData.postValue(workInfo) + + advanceUntilIdle() + + // Verify pending comment was created with the correct file paths + coVerify { pendingSubmissionCommentDao.insert(any()) } + // The workerInputData is not stored directly in the entity - it's constructed from fileUploadInput + // So we can't test it here. Instead, we verify that the entity was created successfully. + assertEquals("domain-3-1-2", pendingCommentSlot.captured.pageId) + } + + @Test + fun `FileUploadEvent UploadStarted does not create duplicate pending comments`() = runTest { + val fileUploadEventsFlow = MutableSharedFlow(replay = 1) + coEvery { fileUploadEventHandler.events } returns fileUploadEventsFlow + + val workInfoLiveData = MutableLiveData() + val workInfo = mockk(relaxed = true) + val workerId = UUID.randomUUID() + every { workInfo.id } returns workerId + every { workInfo.state } returns WorkInfo.State.RUNNING + + val filePaths = listOf("/path/to/file.pdf") + val existingFileUploadInput = FileUploadInputEntity( + workerId = workerId.toString(), + filePaths = filePaths, + courseId = 3L, + assignmentId = 1L, + userId = 2L, + action = "teacher_submission_comment" + ) + + val existingPendingComment = PendingSubmissionCommentWithFileUploadInput( + pendingSubmissionCommentEntity = PendingSubmissionCommentEntity( + pageId = "domain-3-1-2" + ), + fileUploadInput = existingFileUploadInput + ) + + coEvery { fileUploadInputDao.findByWorkerId(workerId.toString()) } returns existingFileUploadInput + coEvery { pendingSubmissionCommentDao.findByPageId(any()) } returns listOf(existingPendingComment) + + createViewModel() + + // Emit the UploadStarted event + fileUploadEventsFlow.emit( + FileUploadEvent.UploadStarted( + uuid = workerId, + workInfoLiveData = workInfoLiveData, + filePaths = filePaths + ) + ) + + advanceUntilIdle() + + // Trigger worker state + workInfoLiveData.postValue(workInfo) + + advanceUntilIdle() + + // Verify no duplicate was created + coVerify(exactly = 0) { fileUploadInputDao.insert(any()) } + coVerify(exactly = 0) { pendingSubmissionCommentDao.insert(any()) } + } + + @Test + fun `FileSelected event updates selectedFilePaths variable`() = runTest { + val fileUploadEventsFlow = MutableSharedFlow(replay = 1) + coEvery { fileUploadEventHandler.events } returns fileUploadEventsFlow + + createViewModel() + + val expectedFilePaths = listOf("/path/to/selected/file.pdf") + + // Emit FileSelected event + fileUploadEventsFlow.emit( + FileUploadEvent.FileSelected(filePaths = expectedFilePaths) + ) + + advanceUntilIdle() + + // The internal selectedFilePaths variable should be updated + // This is tested indirectly by ensuring the next upload uses these paths + viewModel.handleAction( + SpeedGraderCommentsAction.FileUploadStarted( + workInfoLiveData = MutableLiveData() + ) + ) + + advanceUntilIdle() + + // Verify the action was handled (the implementation uses selectedFilePaths) + Assert.assertFalse(viewModel.uiState.value.showAttachmentTypeDialog) + } + + @Test + fun `DialogDismissed event clears file selector dialog`() = runTest { + val fileUploadEventsFlow = MutableSharedFlow(replay = 1) + coEvery { fileUploadEventHandler.events } returns fileUploadEventsFlow + + createViewModel() + + // First show the dialog + viewModel.handleAction(SpeedGraderCommentsAction.ChooseFilesClicked) + Assert.assertNotNull(viewModel.uiState.value.fileSelectorDialogData) + + // Emit DialogDismissed event + fileUploadEventsFlow.emit(FileUploadEvent.DialogDismissed) + + advanceUntilIdle() + + // Verify dialog data is cleared + Assert.assertNull(viewModel.uiState.value.fileSelectorDialogData) + } } \ No newline at end of file From a2d76ddcc3329e526c8c9c816a4e89a86a3bc212 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:13:40 +0200 Subject: [PATCH 39/94] [MBL-19391][Teacher] Fix cursor jumping when entering percentage grades (#3299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed cursor position resetting during percentage grade input by removing the LaunchedEffect that was reformatting the text field on every change. Test plan: See ticket. refs: MBL-19391 affects: Teacher release note: Fixed a bug where the cursor would jump around with percentage grading. 🤖 Generated with Claude Code Co-Authored-By: Claude noreply@anthropic.com --- .../SpeedGraderGradeInteractionTest.kt | 16 +- .../grade/grading/SpeedGraderGradingScreen.kt | 221 ++++++++++-------- 2 files changed, 137 insertions(+), 100 deletions(-) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderGradeInteractionTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderGradeInteractionTest.kt index ca6e5ecaf6..ef88c02d29 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderGradeInteractionTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderGradeInteractionTest.kt @@ -119,7 +119,7 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() { @Test fun correctViewsForPointGradedWithoutRubric() { - goToSpeedGraderGradePage(gradingType = GradingType.points) + goToSpeedGraderGradePage(gradingType = GradingType.points, score = 10.0, grade = "10") speedGraderGradePage.assertSpeedGraderLabelDisplayed() speedGraderGradePage.assertCurrentEnteredScore("10") @@ -156,14 +156,14 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() { speedGraderGradePage.assertFinalGradePointsValueDisplayed("12 / 20 pts") speedGraderGradePage.assertLatePenaltyValueDisplayed("0 pts") - speedGraderGradePage.assertFinalGradeIsDisplayed("12.0") + speedGraderGradePage.assertFinalGradeIsDisplayed("60%") speedGraderGradePage.assertNoRubricCriterionDisplayed() } @Test fun correctViewsForPassFailAssignment() { - goToSpeedGraderGradePage(gradingType = GradingType.pass_fail) + goToSpeedGraderGradePage(gradingType = GradingType.pass_fail, score = 10.0) speedGraderGradePage.assertSpeedGraderLabelDisplayed() speedGraderGradePage.assertCurrentEnteredPassFailScore("10 / 20") @@ -196,7 +196,7 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() { @Test fun correctViewsForGpaScaleAssignment() { - goToSpeedGraderGradePage(GradingType.gpa_scale) + goToSpeedGraderGradePage(GradingType.gpa_scale, score = 10.0) speedGraderGradePage.assertSpeedGraderLabelDisplayed() speedGraderGradePage.assertCurrentEnteredScore("10") speedGraderGradePage.assertPointsPossible("20") @@ -217,7 +217,7 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() { @Test fun correctViewsForLetterGradeAssignment() { - goToSpeedGraderGradePage(gradingType = GradingType.letter_grade) + goToSpeedGraderGradePage(gradingType = GradingType.letter_grade, score = 10.0) speedGraderGradePage.assertSpeedGraderLabelDisplayed() speedGraderGradePage.assertCurrentEnteredScore("10") @@ -320,6 +320,8 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() { gradingType: GradingType = GradingType.points, hasRubric: Boolean = false, pointsPossible: Int = 20, + score: Double = 12.0, + grade: String = "60%", submission: Submission? = null ) { val data = MockCanvas.init(teacherCount = 1, courseCount = 1, favoriteCourseCount = 1, studentCount = 1) @@ -366,8 +368,8 @@ class SpeedGraderGradeInteractionTest : TeacherComposeTest() { userId = student.id, type = "online_text_entry", body = "This is a test submission", - score = 10.0, - grade = "60" + score = score, + grade = grade ) val token = data.tokenFor(teacher)!! diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt index 02321940ba..817144a4c6 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/grading/SpeedGraderGradingScreen.kt @@ -364,8 +364,15 @@ private fun LateHeader( submissionDate: Date?, onLateDaysChange: (Float?) -> Unit ) { - var textFieldValue by remember(daysLate) { - mutableStateOf(numberFormatter.format(daysLate),) + var textFieldValue by remember { + mutableStateOf(numberFormatter.format(daysLate)) + } + + LaunchedEffect(daysLate) { + val apiFormatted = numberFormatter.format(daysLate) + if (textFieldValue != apiFormatted) { + textFieldValue = apiFormatted + } } Row( @@ -424,28 +431,37 @@ private fun LetterGradeGradingTypeInput(uiState: SpeedGraderGradingUiState) { val defaultItem = if (uiState.excused) stringResource(R.string.gradeExcused) else stringResource(R.string.not_graded) - var textFieldScore by remember(uiState.enteredScore) { - mutableStateOf(uiState.enteredScore?.let { - numberFormatter.format( - it - ) - }.orEmpty()) + val initialScore = uiState.enteredScore + val initialGrade = initialScore?.let { + convertScoreToLetterGrade( + it.toDouble(), + uiState.pointsPossible.orDefault(), + uiState.letterGrades + ) + } ?: defaultItem + + var textFieldScore by remember { + mutableStateOf(initialScore?.let { numberFormatter.format(it) }.orEmpty()) } - var selectedGrade by remember( - uiState.enteredScore, - uiState.pointsPossible, - uiState.letterGrades - ) { - mutableStateOf( - uiState.enteredScore?.let { + var selectedGrade by remember { + mutableStateOf(initialGrade) + } + + LaunchedEffect(uiState.enteredScore) { + val apiScore = uiState.enteredScore + val apiFormatted = apiScore?.let { numberFormatter.format(it) }.orEmpty() + + if (textFieldScore != apiFormatted) { + textFieldScore = apiFormatted + selectedGrade = apiScore?.let { convertScoreToLetterGrade( it.toDouble(), uiState.pointsPossible.orDefault(), uiState.letterGrades ) } ?: defaultItem - ) + } } LaunchedEffect(textFieldScore) { @@ -456,7 +472,7 @@ private fun LetterGradeGradingTypeInput(uiState: SpeedGraderGradingUiState) { } LaunchedEffect(selectedGrade) { - if (selectedGrade != uiState.enteredGrade && uiState.letterGrades.any { it.name == selectedGrade }) { + if (selectedGrade != defaultItem && selectedGrade != uiState.enteredGrade && uiState.letterGrades.any { it.name == selectedGrade }) { uiState.onPercentageChange( uiState.letterGrades .find { it.name == selectedGrade } @@ -526,9 +542,17 @@ private fun LetterGradeGradingTypeInput(uiState: SpeedGraderGradingUiState) { @Composable private fun CompleteIncompleteGradingTypeInput(uiState: SpeedGraderGradingUiState) { val haptic = LocalHapticFeedback.current - var grade by remember(uiState.enteredGrade) { + var grade by remember { mutableStateOf(uiState.enteredGrade.orEmpty()) } + + LaunchedEffect(uiState.enteredGrade) { + val apiGrade = uiState.enteredGrade.orEmpty() + if (grade != apiGrade) { + grade = apiGrade + } + } + Column { Row( modifier = Modifier @@ -587,48 +611,58 @@ private fun CompleteIncompleteGradingTypeInput(uiState: SpeedGraderGradingUiStat @OptIn(ExperimentalMaterial3Api::class) @Composable private fun PercentageGradingTypeInput(uiState: SpeedGraderGradingUiState) { - val grade = uiState.enteredGrade?.replace("%", "").orEmpty() - var sliderDrivenScore by remember { mutableFloatStateOf(grade.toFloatOrNull() ?: 0f) } - var textFieldScore by remember(uiState.enteredGrade) { mutableStateOf(grade) } - - val maxScore by remember(uiState.enteredGrade) { - mutableFloatStateOf( - max( - grade.toFloatOrNull() ?: 0f, 100f - ) - ) + val initialGrade = uiState.enteredGrade?.replace("%", "").orEmpty() + val initialGradeAsFloat = initialGrade.toFloatOrNull() ?: 0f + + var sliderDrivenScore by remember { + mutableFloatStateOf(initialGradeAsFloat) } + var textFieldScore by remember { + mutableStateOf(if (initialGradeAsFloat == 0f) "" else numberFormatter.format(initialGradeAsFloat)) + } + + var maxScore by remember { + mutableFloatStateOf(max(initialGradeAsFloat, 100f)) + } + val sliderState = remember(maxScore) { SliderState( - value = sliderDrivenScore.coerceAtLeast(0f), + value = sliderDrivenScore.coerceIn(0f, maxScore), valueRange = 0f..maxScore, ) } - LaunchedEffect(textFieldScore) { - val scoreAsFloat = textFieldScore.toFloatOrNull() - if (scoreAsFloat != uiState.enteredScore) { - uiState.onPercentageChange(scoreAsFloat) + LaunchedEffect(uiState.enteredGrade) { + val apiGrade = uiState.enteredGrade?.replace("%", "")?.toFloatOrNull() + val apiFormatted = apiGrade?.let { if (it == 0f) "" else numberFormatter.format(it) }.orEmpty() + + maxScore = max(apiGrade ?: 0f, 100f) + + if (textFieldScore != apiFormatted) { + textFieldScore = apiFormatted + val newValue = apiGrade ?: 0f + sliderDrivenScore = newValue } } - LaunchedEffect(grade) { - val newScore = grade.toFloatOrNull() - if (textFieldScore != newScore?.toString()) { - textFieldScore = newScore?.let { numberFormatter.format(it) }.orEmpty() + LaunchedEffect(textFieldScore) { + val scoreAsFloat = textFieldScore.toFloatOrNull() ?: 0f + if (sliderDrivenScore != scoreAsFloat) { + sliderDrivenScore = scoreAsFloat + sliderState.value = scoreAsFloat.coerceIn(0f, maxScore) + maxScore = max(scoreAsFloat, maxScore) } - if (sliderDrivenScore != (newScore ?: 0f)) { - sliderDrivenScore = newScore ?: 0f - sliderState.value = (newScore ?: 0f).coerceAtLeast(0f) + val currentPercentage = uiState.enteredGrade?.replace("%", "")?.toFloatOrNull() + if (scoreAsFloat != currentPercentage) { + uiState.onPercentageChange(textFieldScore.toFloatOrNull()) } } LaunchedEffect(sliderState.value) { - val newScoreFromSlider = sliderState.value + val newScoreFromSlider = round(sliderState.value) if (sliderDrivenScore != newScoreFromSlider) { - sliderDrivenScore = round(newScoreFromSlider) - uiState.onPercentageChange(sliderDrivenScore) - textFieldScore = numberFormatter.format(sliderDrivenScore) + sliderDrivenScore = newScoreFromSlider + textFieldScore = numberFormatter.format(newScoreFromSlider) } } @@ -693,80 +727,81 @@ private fun PercentageGradingTypeInput(uiState: SpeedGraderGradingUiState) { private fun PointGradingTypeInput(uiState: SpeedGraderGradingUiState) { val haptic = LocalHapticFeedback.current - val maxScore by remember(uiState.enteredScore) { - mutableFloatStateOf( - max( - (uiState.pointsPossible?.toFloat() ?: 10f), - uiState.enteredScore ?: 0f - ) - ) + val initialScore = uiState.enteredScore ?: 0f + val naturalMaxScore = uiState.pointsPossible?.toFloat() ?: 10f + val initialMaxScore = max(naturalMaxScore, initialScore) + val initialMinScore = min(initialScore, 0f) + val initialPointScale = when { + initialMaxScore <= 10.0 -> 4f + initialMaxScore <= 20.0 -> 2f + else -> 1f } - val pointScale by remember(maxScore) { - mutableFloatStateOf( - when { - maxScore <= 10.0 -> 4f - maxScore <= 20.0 -> 2f - else -> 1f - } - ) + var maxScore by remember { + mutableFloatStateOf(initialMaxScore) } - var sliderDrivenScore by remember(uiState.enteredScore) { - mutableFloatStateOf( - (uiState.enteredScore ?: 0f) * pointScale - ) + var minScore by remember { + mutableFloatStateOf(initialMinScore) } - var textFieldScore by remember(uiState.enteredScore) { - mutableStateOf(uiState.enteredScore?.let { - numberFormatter.format( - it - ) - }.orEmpty()) + + val pointScale by remember { + mutableFloatStateOf(initialPointScale) } - val minScore by remember(uiState.enteredScore) { - mutableFloatStateOf( - min( - uiState.enteredScore ?: 0f, - 0f - ) + var sliderDrivenScore by remember { + mutableFloatStateOf(initialScore * initialPointScale) + } + var textFieldScore by remember { + mutableStateOf( + if (initialScore == 0f) "" else numberFormatter.format(initialScore) ) } - val sliderState = remember(uiState.enteredScore, maxScore, minScore) { + val sliderState = remember(maxScore, minScore) { SliderState( - value = sliderDrivenScore, + value = sliderDrivenScore.coerceIn(minScore * pointScale, maxScore * pointScale), valueRange = minScore * pointScale..maxScore * pointScale, - steps = ((maxScore - minScore).roundToInt() * pointScale.roundToInt() - 1).coerceAtLeast( - 1 - ) + steps = ((maxScore - minScore).roundToInt() * pointScale.roundToInt() - 1).coerceAtLeast(1) ) } - LaunchedEffect(textFieldScore) { - val scoreAsFloat = textFieldScore.toFloatOrNull() - if (scoreAsFloat != uiState.enteredScore) { - uiState.onScoreChange(scoreAsFloat) + LaunchedEffect(uiState.enteredScore) { + val apiScore = uiState.enteredScore + val apiFormatted = apiScore?.let { numberFormatter.format(it) }.orEmpty() + + maxScore = max(apiScore ?: 0f, naturalMaxScore) + minScore = min(apiScore ?: 0f, 0f) + + if (textFieldScore != apiFormatted) { + textFieldScore = apiFormatted + val newValue = (apiScore ?: 0f) * pointScale + sliderDrivenScore = newValue + sliderState.value = newValue } } - LaunchedEffect(uiState.enteredScore) { - val newScore = uiState.enteredScore - if (textFieldScore != newScore?.toString()) { - textFieldScore = newScore?.let { numberFormatter.format(it) }.orEmpty() + LaunchedEffect(textFieldScore) { + val scoreAsFloat = textFieldScore.toFloatOrNull() + val scaledScore = (scoreAsFloat ?: 0f) * pointScale + + if (sliderDrivenScore != scaledScore) { + sliderDrivenScore = scaledScore + maxScore = max(scoreAsFloat ?: 0f, maxScore) + minScore = min(scoreAsFloat ?: 0f, minScore) + sliderState.value = scaledScore.coerceIn(minScore * pointScale, maxScore * pointScale) } - if (sliderDrivenScore != (newScore ?: 0f)) { - sliderDrivenScore = (newScore ?: 0f) * pointScale - sliderState.value = sliderDrivenScore + + if (scoreAsFloat != uiState.enteredScore) { + uiState.onScoreChange(scoreAsFloat) } } LaunchedEffect(sliderState.value) { haptic.performHapticFeedback(HapticFeedbackType.LongPress) val newScoreFromSlider = sliderState.value.roundToInt().toFloat() / pointScale - if (sliderDrivenScore != newScoreFromSlider) { - sliderDrivenScore = newScoreFromSlider + if (sliderDrivenScore != sliderState.value) { + sliderDrivenScore = sliderState.value textFieldScore = numberFormatter.format(newScoreFromSlider) } } From e6020782880f7f49914a18efdc1eea18e414edcc Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:00:01 +0200 Subject: [PATCH 40/94] Fixed a bug in Bookmark List navigation. (Now when a user opens a bookmark from the bookmark list and taps on the back button on toolbar or android back button, it will navigate back to the bookmark list page instead of the dashboard). (#3302) refs: MBL-19386 affects: Student release note: Fixed a bug in Bookmark List navigation. --- .../instructure/student/fragment/BookmarksFragment.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt index ef4ed3995a..af2dbf27e3 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt @@ -39,7 +39,12 @@ import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_BOOKMARKS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.utils.* +import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.hideKeyboard +import com.instructure.pandautils.utils.isTablet +import com.instructure.pandautils.utils.setupAsBackButton +import com.instructure.pandautils.utils.setupAsCloseButton import com.instructure.student.R import com.instructure.student.activity.BookmarkShortcutActivity import com.instructure.student.adapter.BookmarkRecyclerAdapter @@ -129,7 +134,9 @@ class BookmarksFragment : ParentFragment() { recyclerAdapter = BookmarkRecyclerAdapter(requireContext(), isShortcutActivity, object : BookmarkAdapterToFragmentCallback { override fun onRowClicked(bookmark: Bookmark, position: Int, isOpenDetail: Boolean) { bookmarkSelectedCallback(bookmark) - dismiss() + if (isShortcutActivity) { + dismiss() + } } override fun onRefreshFinished() { From 5bf853d15f426b3eb30c9cb14eb057c47b4843f2 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Tue, 14 Oct 2025 15:34:06 +0200 Subject: [PATCH 41/94] [MBL-19252][Student][Teacher] Ungraded items now appear in syllabus (#3305) Test plan: See ticket. refs: MBL-19252 affects: Student, Teacher release note: Fixed a bug where some syllabus items would not appear. --- .../instructure/student/di/SyllabusModule.kt | 12 +- .../calendar/StudentCalendarRepository.kt | 2 +- .../mobius/syllabus/SyllabusEffectHandler.kt | 15 +- .../mobius/syllabus/SyllabusRepository.kt | 11 + .../syllabus/datasource/SyllabusDataSource.kt | 9 + .../datasource/SyllabusLocalDataSource.kt | 32 +- .../datasource/SyllabusNetworkDataSource.kt | 22 +- .../widget/todo/ToDoWidgetRepository.kt | 1 + .../calendar/StudentCalendarRepositoryTest.kt | 6 +- .../syllabus/SyllabusEffectHandlerTest.kt | 40 + .../syllabus/SyllabusLocalDataSourceTest.kt | 153 +- .../syllabus/SyllabusNetworkDataSourceTest.kt | 4 +- .../test/syllabus/SyllabusRepositoryTest.kt | 146 + .../widget/todo/ToDoWidgetRepositoryTest.kt | 6 +- .../ui/rendertests/SyllabusRenderTest.kt | 4 +- .../instructure/teacher/di/SyllabusModule.kt | 22 + .../syllabus/SyllabusEffectHandler.kt | 33 +- .../features/syllabus/SyllabusRepository.kt | 69 + .../features/syllabus/ui/SyllabusFragment.kt | 28 +- .../syllabus/ui/SyllabusRepositoryFragment.kt | 47 + .../fragments/CourseBrowserFragment.kt | 4 +- .../teacher/router/RouteMatcher.kt | 4 +- .../teacher/router/RouteResolver.kt | 6 +- .../syllabus/SyllabusEffectHandlerTest.kt | 228 +- .../syllabus/SyllabusRepositoryTest.kt | 409 ++ .../mockcanvas/endpoints/ApiEndpoint.kt | 23 + .../instructure/canvasapi2/apis/PlannerAPI.kt | 1 + .../canvasapi2/models/PlannerItem.kt | 29 + .../7.json | 6002 +++++++++++++++++ .../room/offline/daos/PlannerItemDaoTest.kt | 228 + .../pandautils/di/OfflineModule.kt | 6 + .../pandautils/di/OfflineSyncModule.kt | 10 +- .../features/offline/sync/CourseSync.kt | 26 +- .../room/offline/OfflineDatabase.kt | 7 +- .../room/offline/OfflineDatabaseMigrations.kt | 30 + .../room/offline/daos/PlannerItemDao.kt | 50 + .../offline/entities/PlannerItemEntity.kt | 140 + 37 files changed, 7767 insertions(+), 98 deletions(-) create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/di/SyllabusModule.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusRepository.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusRepositoryFragment.kt create mode 100644 apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusRepositoryTest.kt create mode 100644 libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/7.json create mode 100644 libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PlannerItemDaoTest.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PlannerItemDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/PlannerItemEntity.kt diff --git a/apps/student/src/main/java/com/instructure/student/di/SyllabusModule.kt b/apps/student/src/main/java/com/instructure/student/di/SyllabusModule.kt index d96591ea7b..cae00bbd8b 100644 --- a/apps/student/src/main/java/com/instructure/student/di/SyllabusModule.kt +++ b/apps/student/src/main/java/com/instructure/student/di/SyllabusModule.kt @@ -2,7 +2,9 @@ package com.instructure.student.di import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.PlannerAPI import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.facade.CourseFacade import com.instructure.pandautils.room.offline.facade.ScheduleItemFacade import com.instructure.pandautils.utils.FeatureFlagProvider @@ -22,18 +24,20 @@ class SyllabusModule { @Provides fun provideNetworkDataSource( courseApi: CourseAPI.CoursesInterface, - calendarEventApi: CalendarEventAPI.CalendarEventInterface + calendarEventApi: CalendarEventAPI.CalendarEventInterface, + plannerApi: PlannerAPI.PlannerInterface ): SyllabusNetworkDataSource { - return SyllabusNetworkDataSource(courseApi, calendarEventApi) + return SyllabusNetworkDataSource(courseApi, calendarEventApi, plannerApi) } @Provides fun provideLocalDataSource( courseSettingsDao: CourseSettingsDao, courseFacade: CourseFacade, - scheduleItemFacade: ScheduleItemFacade + scheduleItemFacade: ScheduleItemFacade, + plannerItemDao: PlannerItemDao ): SyllabusLocalDataSource { - return SyllabusLocalDataSource(courseSettingsDao, courseFacade, scheduleItemFacade) + return SyllabusLocalDataSource(courseSettingsDao, courseFacade, scheduleItemFacade, plannerItemDao) } @Provides diff --git a/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt b/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt index 32f6d25237..b29ed0a0df 100644 --- a/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt +++ b/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt @@ -26,7 +26,6 @@ import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.depaginate import com.instructure.canvasapi2.utils.hasActiveEnrollment -import com.instructure.canvasapi2.utils.isValidTerm import com.instructure.pandautils.features.calendar.CalendarRepository import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao import com.instructure.pandautils.room.calendar.entities.CalendarFilterEntity @@ -54,6 +53,7 @@ class StudentCalendarRepository( startDate, endDate, emptyList(), // We always request all the events for students and filter locally + null, restParams ).depaginate { plannerApi.nextPagePlannerItems(it, restParams) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt index 837b9dd728..6654d06d5e 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt @@ -17,8 +17,6 @@ package com.instructure.student.mobius.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI -import com.instructure.canvasapi2.managers.CalendarEventManager -import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.exhaustive @@ -56,14 +54,25 @@ class SyllabusEffectHandler(private val repository: SyllabusRepository) : Effect val assignments = repository.getCalendarEvents(true, CalendarEventAPI.CalendarEventType.ASSIGNMENT, null, null, contextCodes, effect.forceNetwork) val events = repository.getCalendarEvents(true, CalendarEventAPI.CalendarEventType.CALENDAR, null, null, contextCodes, effect.forceNetwork) + val plannerItems = repository.getPlannerItems(null, null, contextCodes, "all_ungraded_todo_items", effect.forceNetwork) + val endList = mutableListOf() assignments.map { endList.addAll(it) } events.map { endList.addAll(it) } + plannerItems.map { items -> + // Filter out assignments, quizzes, and calendar events as they're already fetched above + val filteredItems = items.filter { + it.plannableType != com.instructure.canvasapi2.models.PlannableType.ASSIGNMENT && + it.plannableType != com.instructure.canvasapi2.models.PlannableType.QUIZ && + it.plannableType != com.instructure.canvasapi2.models.PlannableType.CALENDAR_EVENT + } + endList.addAll(filteredItems.map { it.toScheduleItem() }) + } endList.sort() - summaryResult = if (assignments.isFail && events.isFail) { + summaryResult = if (assignments.isFail && events.isFail && plannerItems.isFail) { DataResult.Fail((assignments as? DataResult.Fail)?.failure) } else { DataResult.Success(endList) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusRepository.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusRepository.kt index 0b65dfd7ad..ab64229298 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusRepository.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusRepository.kt @@ -21,6 +21,7 @@ package com.instructure.student.mobius.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.pandautils.repository.Repository @@ -55,4 +56,14 @@ class SyllabusRepository( ): DataResult> { return dataSource().getCalendarEvents(allEvents, type, startDate, endDate, canvasContexts, forceNetwork) } + + suspend fun getPlannerItems( + startDate: String?, + endDate: String?, + contextCodes: List, + filter: String?, + forceNetwork: Boolean + ): DataResult> { + return dataSource().getPlannerItems(startDate, endDate, contextCodes, filter, forceNetwork) + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusDataSource.kt index 947c812ca9..6a02a472bd 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusDataSource.kt @@ -21,6 +21,7 @@ package com.instructure.student.mobius.syllabus.datasource import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult @@ -38,4 +39,12 @@ interface SyllabusDataSource { canvasContexts: List, forceNetwork: Boolean ): DataResult> + + suspend fun getPlannerItems( + startDate: String?, + endDate: String?, + contextCodes: List, + filter: String?, + forceNetwork: Boolean + ): DataResult> } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusLocalDataSource.kt index ec16f6e37b..ddd9e0a56d 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusLocalDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusLocalDataSource.kt @@ -21,16 +21,19 @@ package com.instructure.student.mobius.syllabus.datasource import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.facade.CourseFacade import com.instructure.pandautils.room.offline.facade.ScheduleItemFacade class SyllabusLocalDataSource( private val courseSettingsDao: CourseSettingsDao, private val courseFacade: CourseFacade, - private val scheduleItemFacade: ScheduleItemFacade + private val scheduleItemFacade: ScheduleItemFacade, + private val plannerItemDao: PlannerItemDao ) : SyllabusDataSource { override suspend fun getCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { @@ -58,4 +61,31 @@ class SyllabusLocalDataSource( } } + + override suspend fun getPlannerItems( + startDate: String?, + endDate: String?, + contextCodes: List, + filter: String?, + forceNetwork: Boolean + ): DataResult> { + return try { + val courseIds = contextCodes.mapNotNull { contextCode -> + val parts = contextCode.split("_") + if (parts.size == 2 && parts[0] == "course") { + parts[1].toLongOrNull() + } else null + } + + val plannerItems = if (courseIds.isNotEmpty()) { + plannerItemDao.findByCourseIds(courseIds).map { it.toApiModel() } + } else { + emptyList() + } + + DataResult.Success(plannerItems) + } catch (e: Exception) { + DataResult.Fail() + } + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusNetworkDataSource.kt index a237849f11..7eb7c7ded7 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusNetworkDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusNetworkDataSource.kt @@ -20,16 +20,19 @@ package com.instructure.student.mobius.syllabus.datasource import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.PlannerAPI import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.depaginate class SyllabusNetworkDataSource( private val courseApi: CourseAPI.CoursesInterface, - private val calendarEventApi: CalendarEventAPI.CalendarEventInterface + private val calendarEventApi: CalendarEventAPI.CalendarEventInterface, + private val plannerApi: PlannerAPI.PlannerInterface ) : SyllabusDataSource { override suspend fun getCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { @@ -60,4 +63,21 @@ class SyllabusNetworkDataSource( restParams ).depaginate { calendarEventApi.next(it, restParams) } } + + override suspend fun getPlannerItems( + startDate: String?, + endDate: String?, + contextCodes: List, + filter: String?, + forceNetwork: Boolean + ): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return plannerApi.getPlannerItems( + startDate, + endDate, + contextCodes, + filter, + restParams + ).depaginate { plannerApi.nextPagePlannerItems(it, restParams) } + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/widget/todo/ToDoWidgetRepository.kt b/apps/student/src/main/java/com/instructure/student/widget/todo/ToDoWidgetRepository.kt index 3a101244e2..107090320a 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/todo/ToDoWidgetRepository.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/todo/ToDoWidgetRepository.kt @@ -49,6 +49,7 @@ class ToDoWidgetRepository( startDate, endDate, contextCodes, + null, restParams ).depaginate { plannerApi.nextPagePlannerItems(it, restParams) diff --git a/apps/student/src/test/java/com/instructure/student/features/calendar/StudentCalendarRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/calendar/StudentCalendarRepositoryTest.kt index e943963d23..c70030d78a 100644 --- a/apps/student/src/test/java/com/instructure/student/features/calendar/StudentCalendarRepositoryTest.kt +++ b/apps/student/src/test/java/com/instructure/student/features/calendar/StudentCalendarRepositoryTest.kt @@ -55,7 +55,7 @@ class StudentCalendarRepositoryTest { @Test(expected = IllegalStateException::class) fun `Throw exception when request fails`() = runTest { - coEvery { plannerApi.getPlannerItems(any(), any(), any(), any()) } returns DataResult.Fail() + coEvery { plannerApi.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Fail() calendarRepository.getPlannerItems("2023-1-1", "2023-1-2", emptyList(), true) } @@ -73,7 +73,7 @@ class StudentCalendarRepositoryTest { createPlannerItem(2, 6, PlannableType.CALENDAR_EVENT) ) - coEvery { plannerApi.getPlannerItems(any(), any(), any(), any()) } returns DataResult.Success(plannerItems) + coEvery { plannerApi.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success(plannerItems) val result = calendarRepository.getPlannerItems("2023-1-1", "2023-1-2", emptyList(), true) @@ -92,7 +92,7 @@ class StudentCalendarRepositoryTest { createPlannerItem(2, 6, PlannableType.CALENDAR_EVENT) ) - coEvery { plannerApi.getPlannerItems(any(), any(), any(), any()) } returns DataResult.Success( + coEvery { plannerApi.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success( plannerItems1, linkHeaders = LinkHeaders(nextUrl = "next") ) diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt index fe6928bb8c..d02215b83a 100644 --- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt @@ -105,6 +105,16 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Fail() + coEvery { + syllabusRepository.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + connection.accept(SyllabusEffect.LoadData(courseId, false)) verify(timeout = 100) { @@ -170,6 +180,16 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Success(calendarEvents) + coEvery { + syllabusRepository.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + connection.accept(SyllabusEffect.LoadData(courseId, false)) verify(timeout = 100) { @@ -217,6 +237,16 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Fail() + coEvery { + syllabusRepository.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + connection.accept(SyllabusEffect.LoadData(courseId, false)) verify(timeout = 100) { @@ -264,6 +294,16 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Success(calendarEvents) + coEvery { + syllabusRepository.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + connection.accept(SyllabusEffect.LoadData(courseId, false)) verify(timeout = 100) { diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt index 46fa94252d..82d63d6fd5 100644 --- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt @@ -21,12 +21,18 @@ package com.instructure.student.test.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Plannable +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.entities.CourseSettingsEntity +import com.instructure.pandautils.room.offline.entities.PlannerItemEntity import com.instructure.pandautils.room.offline.facade.CourseFacade import com.instructure.pandautils.room.offline.facade.ScheduleItemFacade +import java.util.Date import com.instructure.student.mobius.syllabus.datasource.SyllabusLocalDataSource import io.mockk.coEvery import io.mockk.coVerify @@ -41,12 +47,13 @@ class SyllabusLocalDataSourceTest { private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true) private val courseFacade: CourseFacade = mockk(relaxed = true) private val scheduleItemFacade: ScheduleItemFacade = mockk(relaxed = true) + private val plannerItemDao: PlannerItemDao = mockk(relaxed = true) private lateinit var syllabusLocalDataSource: SyllabusLocalDataSource @Before fun setup() { - syllabusLocalDataSource = SyllabusLocalDataSource(courseSettingsDao, courseFacade, scheduleItemFacade) + syllabusLocalDataSource = SyllabusLocalDataSource(courseSettingsDao, courseFacade, scheduleItemFacade, plannerItemDao) } @Test @@ -120,4 +127,148 @@ class SyllabusLocalDataSourceTest { assertEquals(DataResult.Fail(), result) } + + @Test + fun `Return planner items for single course`() = runTest { + val plannable = Plannable( + id = 1L, + title = "Assignment 1", + courseId = 1L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = 1L, + todoDate = null, + startAt = null, + endAt = null, + details = "Assignment details", + allDay = false + ) + val plannerItem = PlannerItem( + courseId = 1L, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course 1", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable, + plannableDate = Date(), + htmlUrl = "https://example.com", + submissionState = null, + newActivity = false + ) + val entity = PlannerItemEntity(plannerItem, 1L) + val expected = listOf(plannerItem) + + coEvery { plannerItemDao.findByCourseIds(listOf(1L)) } returns listOf(entity) + + val result = syllabusLocalDataSource.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Success(expected).isSuccess, result.isSuccess) + coVerify(exactly = 1) { + plannerItemDao.findByCourseIds(listOf(1L)) + } + } + + @Test + fun `Return planner items for multiple courses`() = runTest { + val plannable1 = Plannable( + id = 1L, + title = "Assignment 1", + courseId = 1L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = 1L, + todoDate = null, + startAt = null, + endAt = null, + details = "Assignment details", + allDay = false + ) + val plannerItem1 = PlannerItem( + courseId = 1L, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course 1", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable1, + plannableDate = Date(), + htmlUrl = "https://example.com", + submissionState = null, + newActivity = false + ) + val entity1 = PlannerItemEntity(plannerItem1, 1L) + + val plannable2 = Plannable( + id = 2L, + title = "Assignment 2", + courseId = 2L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = 2L, + todoDate = null, + startAt = null, + endAt = null, + details = "Assignment details", + allDay = false + ) + val plannerItem2 = PlannerItem( + courseId = 2L, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course 2", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable2, + plannableDate = Date(), + htmlUrl = "https://example.com", + submissionState = null, + newActivity = false + ) + val entity2 = PlannerItemEntity(plannerItem2, 2L) + + coEvery { plannerItemDao.findByCourseIds(listOf(1L, 2L)) } returns listOf(entity1, entity2) + + val result = syllabusLocalDataSource.getPlannerItems(null, null, listOf("course_1", "course_2"), null, false) + + assertEquals(DataResult.Success(listOf(plannerItem1, plannerItem2)).isSuccess, result.isSuccess) + coVerify(exactly = 1) { + plannerItemDao.findByCourseIds(listOf(1L, 2L)) + } + } + + @Test + fun `Return empty list when no context codes provided`() = runTest { + val result = syllabusLocalDataSource.getPlannerItems(null, null, emptyList(), null, false) + + assertEquals(DataResult.Success(emptyList()), result) + coVerify(exactly = 0) { + plannerItemDao.findByCourseIds(any()) + } + } + + @Test + fun `Return empty list for invalid context codes`() = runTest { + val result = syllabusLocalDataSource.getPlannerItems(null, null, listOf("user_1", "group_2"), null, false) + + assertEquals(DataResult.Success(emptyList()), result) + coVerify(exactly = 0) { + plannerItemDao.findByCourseIds(any()) + } + } + + @Test + fun `Return failed data result on planner items error`() = runTest { + coEvery { plannerItemDao.findByCourseIds(any()) } throws Exception() + + val result = syllabusLocalDataSource.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Fail(), result) + } } \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt index 6f926884be..94cee5dfd4 100644 --- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt @@ -20,6 +20,7 @@ package com.instructure.student.test.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.PlannerAPI import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings @@ -40,12 +41,13 @@ class SyllabusNetworkDataSourceTest { private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) private val calendarEventApi: CalendarEventAPI.CalendarEventInterface = mockk(relaxed = true) + private val plannerApi: PlannerAPI.PlannerInterface = mockk(relaxed = true) private lateinit var syllabusNetworkDataSource: SyllabusNetworkDataSource @Before fun setup() { - syllabusNetworkDataSource = SyllabusNetworkDataSource(courseApi, calendarEventApi) + syllabusNetworkDataSource = SyllabusNetworkDataSource(courseApi, calendarEventApi, plannerApi) } @Test diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt index dbff3a6049..b9500cb699 100644 --- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt @@ -21,8 +21,12 @@ package com.instructure.student.test.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Plannable +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult +import java.util.Date import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.student.mobius.syllabus.SyllabusRepository @@ -227,4 +231,146 @@ class SyllabusRepositoryTest { assertEquals(DataResult.Fail(), result) } + + @Test + fun `Return planner items when online`() = runTest { + val plannable = Plannable( + id = 1L, + title = "Assignment 1", + courseId = 1L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = 1L, + todoDate = null, + startAt = null, + endAt = null, + details = "Assignment details", + allDay = false + ) + val expected = listOf( + PlannerItem( + courseId = 1L, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course 1", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable, + plannableDate = Date(), + htmlUrl = "https://example.com", + submissionState = null, + newActivity = false + ) + ) + + every { networkStateProvider.isOnline() } returns true + + coEvery { + syllabusNetworkDataSource.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(expected) + + val result = repository.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { syllabusNetworkDataSource.getPlannerItems(null, null, listOf("course_1"), null, false) } + coVerify(exactly = 0) { syllabusLocalDataSource.getPlannerItems(any(), any(), any(), any(), any()) } + } + + @Test + fun `Return planner items when offline`() = runTest { + val plannable = Plannable( + id = 1L, + title = "Assignment 1", + courseId = 1L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = 1L, + todoDate = null, + startAt = null, + endAt = null, + details = "Assignment details", + allDay = false + ) + val expected = listOf( + PlannerItem( + courseId = 1L, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course 1", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable, + plannableDate = Date(), + htmlUrl = "https://example.com", + submissionState = null, + newActivity = false + ) + ) + + every { networkStateProvider.isOnline() } returns false + + coEvery { + syllabusLocalDataSource.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(expected) + + val result = repository.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { syllabusLocalDataSource.getPlannerItems(null, null, listOf("course_1"), null, false) } + coVerify(exactly = 0) { syllabusNetworkDataSource.getPlannerItems(any(), any(), any(), any(), any()) } + } + + @Test + fun `Return failed result for planner items on network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { + syllabusNetworkDataSource.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + val result = repository.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return failed result for planner items on db error`() = runTest { + every { networkStateProvider.isOnline() } returns false + + coEvery { + syllabusLocalDataSource.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + val result = repository.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Fail(), result) + } } \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/widget/todo/ToDoWidgetRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/widget/todo/ToDoWidgetRepositoryTest.kt index e5f36044db..4781385a5b 100644 --- a/apps/student/src/test/java/com/instructure/student/widget/todo/ToDoWidgetRepositoryTest.kt +++ b/apps/student/src/test/java/com/instructure/student/widget/todo/ToDoWidgetRepositoryTest.kt @@ -49,7 +49,7 @@ class ToDoWidgetRepositoryTest { @Test fun `Returns failed result when planner api request fails`() = runTest { - coEvery { plannerApi.getPlannerItems(any(), any(), any(), any()) } returns DataResult.Fail() + coEvery { plannerApi.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Fail() val result = repository.getPlannerItems("2023-1-1", "2023-1-2", emptyList(), true) @@ -69,7 +69,7 @@ class ToDoWidgetRepositoryTest { createPlannerItem(2, 6, PlannableType.CALENDAR_EVENT) ) - coEvery { plannerApi.getPlannerItems(any(), any(), any(), any()) } returns DataResult.Success(plannerItems) + coEvery { plannerApi.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success(plannerItems) val result = repository.getPlannerItems("2023-1-1", "2023-1-2", emptyList(), true) @@ -88,7 +88,7 @@ class ToDoWidgetRepositoryTest { createPlannerItem(2, 6, PlannableType.CALENDAR_EVENT) ) - coEvery { plannerApi.getPlannerItems(any(), any(), any(), any()) } returns DataResult.Success( + coEvery { plannerApi.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success( plannerItems1, linkHeaders = LinkHeaders(nextUrl = "next") ) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/SyllabusRenderTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/SyllabusRenderTest.kt index 253fb56a5b..e00956492e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/SyllabusRenderTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/SyllabusRenderTest.kt @@ -22,7 +22,7 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.teacher.features.syllabus.SyllabusModel -import com.instructure.teacher.features.syllabus.ui.SyllabusFragment +import com.instructure.teacher.features.syllabus.ui.SyllabusRepositoryFragment import com.instructure.teacher.ui.rendertests.renderpages.SyllabusRenderPage import com.instructure.teacher.ui.utils.TeacherRenderTest import com.spotify.mobius.runners.WorkRunner @@ -147,7 +147,7 @@ class SyllabusRenderTest : TeacherRenderTest() { override fun post(runnable: Runnable) = Unit } val canvasContext = model.course?.dataOrNull ?: Course(id = model.courseId) - val fragment = SyllabusFragment.newInstance(canvasContext)!!.apply { + val fragment = SyllabusRepositoryFragment.newInstance(canvasContext)!!.apply { overrideInitModel = model loopMod = { it.effectRunner { emptyEffectRunner } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/SyllabusModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/SyllabusModule.kt new file mode 100644 index 0000000000..b71ebc2de3 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/di/SyllabusModule.kt @@ -0,0 +1,22 @@ +package com.instructure.teacher.di + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.apis.PlannerAPI +import com.instructure.teacher.features.syllabus.SyllabusRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class SyllabusModule { + + @Provides + fun provideSyllabusRepository( + plannerApi: PlannerAPI.PlannerInterface, + calendarEventApi: CalendarEventAPI.CalendarEventInterface + ): SyllabusRepository { + return SyllabusRepository(plannerApi, calendarEventApi) + } +} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandler.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandler.kt index d95af4362d..d626ea1b82 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandler.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandler.kt @@ -17,17 +17,20 @@ package com.instructure.teacher.features.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI -import com.instructure.canvasapi2.managers.CalendarEventManager import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.exhaustive import com.instructure.teacher.features.syllabus.ui.SyllabusView import com.instructure.teacher.mobius.common.ui.EffectHandler +import kotlinx.coroutines.async import kotlinx.coroutines.launch -class SyllabusEffectHandler : EffectHandler() { +class SyllabusEffectHandler( + private val repository: SyllabusRepository +) : EffectHandler() { override fun accept(effect: SyllabusEffect) { when (effect) { @@ -59,16 +62,14 @@ class SyllabusEffectHandler : EffectHandler>, eventsResult: DataResult>): DataResult.Success> { + private fun createSuccessResult( + assignmentsResult: DataResult>, + eventsResult: DataResult>, + plannerItemsResult: DataResult> + ): DataResult.Success> { val assignments = assignmentsResult.dataOrNull ?: emptyList() val events = eventsResult.dataOrNull ?: emptyList() - val combinedList = (assignments + events).sorted() + val plannerItems = plannerItemsResult.dataOrNull + ?.filter { + it.plannableType != com.instructure.canvasapi2.models.PlannableType.ASSIGNMENT && + it.plannableType != com.instructure.canvasapi2.models.PlannableType.CALENDAR_EVENT + } + ?.map { it.toScheduleItem() } ?: emptyList() + val combinedList = (assignments + events + plannerItems).sorted() return DataResult.Success(combinedList) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusRepository.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusRepository.kt new file mode 100644 index 0000000000..75033839c4 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusRepository.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.teacher.features.syllabus + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.apis.PlannerAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate +import javax.inject.Inject + +class SyllabusRepository @Inject constructor( + private val plannerApi: PlannerAPI.PlannerInterface, + private val calendarEventApi: CalendarEventAPI.CalendarEventInterface +) { + + suspend fun getPlannerItems( + startDate: String?, + endDate: String?, + contextCodes: List, + filter: String?, + forceNetwork: Boolean + ): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return plannerApi.getPlannerItems( + startDate, + endDate, + contextCodes, + filter, + restParams + ).depaginate { plannerApi.nextPagePlannerItems(it, restParams) } + } + + suspend fun getCalendarEvents( + allEvents: Boolean, + type: CalendarEventAPI.CalendarEventType, + startDate: String?, + endDate: String?, + contextCodes: List, + forceNetwork: Boolean + ): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return calendarEventApi.getCalendarEvents( + allEvents, + type.apiName, + startDate, + endDate, + contextCodes, + restParams + ).depaginate { calendarEventApi.next(it, restParams) } + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusFragment.kt index 99157d601a..dd808b4416 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusFragment.kt @@ -18,7 +18,6 @@ package com.instructure.teacher.features.syllabus.ui import android.view.LayoutInflater import android.view.ViewGroup -import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.canvasapi2.utils.pageview.PageViewUrlParam @@ -26,19 +25,26 @@ import com.instructure.pandautils.analytics.SCREEN_VIEW_SYLLABUS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ParcelableArg -import com.instructure.pandautils.utils.withArgs import com.instructure.teacher.databinding.FragmentSyllabusBinding -import com.instructure.teacher.features.syllabus.* +import com.instructure.teacher.features.syllabus.SyllabusEffect +import com.instructure.teacher.features.syllabus.SyllabusEffectHandler +import com.instructure.teacher.features.syllabus.SyllabusEvent +import com.instructure.teacher.features.syllabus.SyllabusModel +import com.instructure.teacher.features.syllabus.SyllabusPresenter +import com.instructure.teacher.features.syllabus.SyllabusRepository +import com.instructure.teacher.features.syllabus.SyllabusUpdate import com.instructure.teacher.mobius.common.ui.MobiusFragment @PageView("{canvasContext}/assignments/syllabus") @ScreenView(SCREEN_VIEW_SYLLABUS) -class SyllabusFragment : MobiusFragment() { +abstract class SyllabusFragment : MobiusFragment() { @get:PageViewUrlParam("canvasContext") val canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - override fun makeEffectHandler() = SyllabusEffectHandler() + protected abstract fun getRepository(): SyllabusRepository + + override fun makeEffectHandler() = SyllabusEffectHandler(getRepository()) override fun makeUpdate() = SyllabusUpdate() @@ -57,16 +63,4 @@ class SyllabusFragment : MobiusFragment. + * + */ + +package com.instructure.teacher.features.syllabus.ui + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.withArgs +import com.instructure.teacher.features.syllabus.SyllabusRepository +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class SyllabusRepositoryFragment : SyllabusFragment() { + + @Inject + lateinit var syllabusRepository: SyllabusRepository + + override fun getRepository() = syllabusRepository + + companion object { + fun newInstance(canvasContext: CanvasContext?) = if (isValidRoute(canvasContext)) createFragmentWithCanvasContext(canvasContext) else null + + private fun isValidRoute(canvasContext: CanvasContext?) = canvasContext is Course + + private fun createFragmentWithCanvasContext(canvasContext: CanvasContext?): SyllabusRepositoryFragment { + return SyllabusRepositoryFragment().withArgs { + putParcelable(Const.CANVAS_CONTEXT, canvasContext) + } + } + } +} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt index 775d78a9e8..6c6389925d 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt @@ -59,7 +59,7 @@ import com.instructure.teacher.databinding.FragmentCourseBrowserBinding import com.instructure.teacher.events.CourseUpdatedEvent import com.instructure.teacher.factory.CourseBrowserPresenterFactory import com.instructure.teacher.features.modules.list.ui.ModuleListFragment -import com.instructure.teacher.features.syllabus.ui.SyllabusFragment +import com.instructure.teacher.features.syllabus.ui.SyllabusRepositoryFragment import com.instructure.teacher.holders.CourseBrowserViewHolder import com.instructure.teacher.presenters.CourseBrowserPresenter import com.instructure.teacher.router.RouteMatcher @@ -270,7 +270,7 @@ class CourseBrowserFragment : BaseSyncFragment< presenter.handleStudentViewClick() } Tab.SYLLABUS_ID -> { - RouteMatcher.route(requireActivity(), Route(SyllabusFragment::class.java, presenter.canvasContext, presenter.canvasContext.makeBundle())) + RouteMatcher.route(requireActivity(), Route(SyllabusRepositoryFragment::class.java, presenter.canvasContext, presenter.canvasContext.makeBundle())) } else -> { if (tab.type == Tab.TYPE_EXTERNAL) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt index c680f175e9..8db4af399a 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt @@ -85,7 +85,7 @@ import com.instructure.teacher.features.modules.list.ui.ModuleListFragment import com.instructure.teacher.features.modules.progression.ModuleProgressionFragment import com.instructure.teacher.features.postpolicies.ui.PostPolicyFragment import com.instructure.teacher.features.syllabus.edit.EditSyllabusFragment -import com.instructure.teacher.features.syllabus.ui.SyllabusFragment +import com.instructure.teacher.features.syllabus.ui.SyllabusRepositoryFragment import com.instructure.teacher.fragments.AnnouncementListFragment import com.instructure.teacher.fragments.AssigneeListFragment import com.instructure.teacher.fragments.AttendanceListFragment @@ -141,7 +141,7 @@ object RouteMatcher : BaseRouteMatcher() { routes.add(Route(courseOrGroup("/"), DashboardFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id"), CourseBrowserFragment::class.java)) - routes.add(Route(courseOrGroup("/:course_id/assignments/syllabus"), SyllabusFragment::class.java)) + routes.add(Route(courseOrGroup("/:course_id/assignments/syllabus"), SyllabusRepositoryFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id/modules/:module_id"), ModuleListFragment::class.java)) routes.add( diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt index 5fb0784890..e338ef5c05 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt @@ -36,7 +36,7 @@ import com.instructure.teacher.features.modules.list.ui.ModuleListFragment import com.instructure.teacher.features.modules.progression.ModuleProgressionFragment import com.instructure.teacher.features.postpolicies.ui.PostPolicyFragment import com.instructure.teacher.features.syllabus.edit.EditSyllabusFragment -import com.instructure.teacher.features.syllabus.ui.SyllabusFragment +import com.instructure.teacher.features.syllabus.ui.SyllabusRepositoryFragment import com.instructure.teacher.fragments.AnnouncementListFragment import com.instructure.teacher.fragments.AssigneeListFragment import com.instructure.teacher.fragments.AttendanceListFragment @@ -218,8 +218,8 @@ object RouteResolver { fragment = EditFileFolderFragment.newInstance(route.arguments) } else if (CreateOrEditPageDetailsFragment::class.java.isAssignableFrom(cls)) { fragment = CreateOrEditPageDetailsFragment.newInstance(route.arguments) - } else if (SyllabusFragment::class.java.isAssignableFrom(cls)) { - fragment = SyllabusFragment.newInstance(canvasContext ?: route.canvasContext) + } else if (SyllabusRepositoryFragment::class.java.isAssignableFrom(cls)) { + fragment = SyllabusRepositoryFragment.newInstance(canvasContext ?: route.canvasContext) } else if (EditSyllabusFragment::class.java.isAssignableFrom(cls)) { fragment = EditSyllabusFragment.newInstance(route.arguments) } else if (EventFragment::class.java.isAssignableFrom(cls)) { diff --git a/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandlerTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandlerTest.kt index 526f9c469c..c438c258b0 100644 --- a/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandlerTest.kt +++ b/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandlerTest.kt @@ -16,22 +16,36 @@ */ package com.instructure.teacher.features.syllabus -import com.instructure.canvasapi2.apis.CalendarEventAPI -import com.instructure.canvasapi2.managers.CalendarEventManager import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Plannable +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.toApiString import com.instructure.teacher.features.syllabus.ui.SyllabusView import com.spotify.mobius.functions.Consumer -import io.mockk.* +import io.mockk.coEvery +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.unmockkAll +import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Test -import java.util.* +import java.util.Date import java.util.concurrent.Executors private const val COURSE_ID: Long = 1L @@ -39,8 +53,9 @@ private const val COURSE_ID: Long = 1L class SyllabusEffectHandlerTest { private val view: SyllabusView = mockk(relaxed = true) private val eventConsumer: Consumer = mockk(relaxed = true) + private val repository: SyllabusRepository = mockk(relaxed = true) - private val effectHandler = SyllabusEffectHandler().apply { + private val effectHandler = SyllabusEffectHandler(repository).apply { view = this@SyllabusEffectHandlerTest.view connect(eventConsumer) } @@ -54,6 +69,13 @@ class SyllabusEffectHandlerTest { fun setup() { Dispatchers.setMain(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) course = Course(id = COURSE_ID) + mockkObject(ApiPrefs) + every { ApiPrefs.fullDomain } returns "https://test.instructure.com" + } + + @After + fun tearDown() { + unmockkAll() } @Test @@ -93,10 +115,8 @@ class SyllabusEffectHandlerTest { coEvery { await() } returns DataResult.Success(permissions) } - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), any(), any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail() - } + coEvery { repository.getCalendarEvents(any(), any(), any(), any(), any(), any()) } returns DataResult.Fail() + coEvery { repository.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Fail() // When effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) @@ -139,13 +159,12 @@ class SyllabusEffectHandlerTest { coEvery { await() } returns DataResult.Success(permissions) } - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignments) - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(calendarEvents) - } + coEvery { repository.getCalendarEvents(true, any(), any(), any(), any(), false) } returnsMany listOf( + DataResult.Success(assignments), + DataResult.Success(calendarEvents) + ) + + coEvery { repository.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success(emptyList()) // When effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) @@ -181,13 +200,12 @@ class SyllabusEffectHandlerTest { coEvery { await() } returns DataResult.Success(permissions) } - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignments) - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail() - } + coEvery { repository.getCalendarEvents(true, any(), any(), any(), any(), false) } returnsMany listOf( + DataResult.Success(assignments), + DataResult.Fail() + ) + + coEvery { repository.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success(emptyList()) // When effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) @@ -217,13 +235,12 @@ class SyllabusEffectHandlerTest { coEvery { await() } returns DataResult.Success(permissions) } - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail() - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(calendarEvents) - } + coEvery { repository.getCalendarEvents(true, any(), any(), any(), any(), false) } returnsMany listOf( + DataResult.Fail(), + DataResult.Success(calendarEvents) + ) + + coEvery { repository.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success(emptyList()) // When effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) @@ -254,14 +271,6 @@ class SyllabusEffectHandlerTest { coEvery { await() } returns DataResult.Success(permissions) } - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignments) - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(calendarEvents) - } - // When effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) @@ -274,6 +283,147 @@ class SyllabusEffectHandlerTest { confirmVerified(eventConsumer) } + @Test + fun `LoadData should filter out assignment and calendar event type planner items to avoid duplicates`() { + // Given + val now = Date().time + val assignment = ScheduleItem( + itemId = "123", + itemType = ScheduleItem.Type.TYPE_ASSIGNMENT, + startAt = Date(now + 1000).toApiString() + ) + val calendarEvent = ScheduleItem( + itemId = "789", + itemType = ScheduleItem.Type.TYPE_CALENDAR, + startAt = Date(now + 2000).toApiString() + ) + val plannerAssignment = PlannerItem( + courseId = COURSE_ID, + groupId = null, + userId = null, + contextType = "Course", + contextName = "Test Course", + plannableType = PlannableType.ASSIGNMENT, + plannable = Plannable( + id = 123, + title = "Assignment", + courseId = COURSE_ID, + groupId = null, + userId = null, + pointsPossible = null, + dueAt = null, + assignmentId = null, + todoDate = null, + startAt = null, + endAt = null, + details = null, + allDay = null + ), + plannableDate = Date(), + htmlUrl = null, + submissionState = null, + newActivity = false + ) + val plannerCalendarEvent = PlannerItem( + courseId = COURSE_ID, + groupId = null, + userId = null, + contextType = "Course", + contextName = "Test Course", + plannableType = PlannableType.CALENDAR_EVENT, + plannable = Plannable( + id = 789, + title = "Calendar Event", + courseId = COURSE_ID, + groupId = null, + userId = null, + pointsPossible = null, + dueAt = null, + assignmentId = null, + todoDate = null, + startAt = null, + endAt = null, + details = null, + allDay = null + ), + plannableDate = Date(), + htmlUrl = null, + submissionState = null, + newActivity = false + ) + val plannerQuiz = PlannerItem( + courseId = COURSE_ID, + groupId = null, + userId = null, + contextType = "Course", + contextName = "Test Course", + plannableType = PlannableType.QUIZ, + plannable = Plannable( + id = 456, + title = "Quiz", + courseId = COURSE_ID, + groupId = null, + userId = null, + pointsPossible = null, + dueAt = null, + assignmentId = null, + todoDate = Date(now + 3000).toApiString(), + startAt = null, + endAt = null, + details = null, + allDay = null + ), + plannableDate = Date(now + 3000), + htmlUrl = null, + submissionState = null, + newActivity = false + ) + + mockkObject(CourseManager) + every { CourseManager.getCourseWithSyllabusAsync(COURSE_ID, false) } returns mockk { + coEvery { await() } returns DataResult.Success(course) + } + every { CourseManager.getCourseSettingsAsync(COURSE_ID, false) } returns mockk { + coEvery { await() } returns DataResult.Success(CourseSettings(courseSummary = true)) + } + every { CourseManager.getPermissionsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(permissions) + } + + coEvery { repository.getCalendarEvents(true, any(), any(), any(), any(), false) } returnsMany listOf( + DataResult.Success(listOf(assignment)), + DataResult.Success(listOf(calendarEvent)) + ) + + coEvery { repository.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success(listOf(plannerAssignment, plannerCalendarEvent, plannerQuiz)) + + // When + effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) + + // Then + // Capture what was actually called + val slot = slot() + verify(timeout = 100) { + eventConsumer.accept(capture(slot)) + } + + // Verify the captured event + val event = slot.captured + assert(event.course is DataResult.Success) + assert(event.events is DataResult.Success) + assert(event.permissionsResult is DataResult.Success) + assert(event.summaryAllowed == true) + + // Verify we have exactly 3 items: assignment, calendar event, and quiz (no planner duplicates) + val items = event.events.dataOrNull!! + assert(items.size == 3) { "Expected 3 items, got ${items.size}" } + assert(items.count { it.itemType == ScheduleItem.Type.TYPE_ASSIGNMENT } == 1) { "Expected 1 assignment" } + assert(items.count { it.itemType == ScheduleItem.Type.TYPE_CALENDAR } == 1) { "Expected 1 calendar event" } + assert(items.count { it.itemType == ScheduleItem.Type.TYPE_SYLLABUS } == 1) { "Expected 1 quiz/syllabus item" } + + confirmVerified(eventConsumer) + } + @Test fun `ShowAssignmentView results in view calling showAssignmentView`() { // Given diff --git a/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusRepositoryTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusRepositoryTest.kt new file mode 100644 index 0000000000..3fd20f4752 --- /dev/null +++ b/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusRepositoryTest.kt @@ -0,0 +1,409 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.teacher.features.syllabus + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.apis.PlannerAPI +import com.instructure.canvasapi2.models.Plannable +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.toApiString +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.Date + +class SyllabusRepositoryTest { + + private val plannerApi: PlannerAPI.PlannerInterface = mockk(relaxed = true) + private val calendarEventApi: CalendarEventAPI.CalendarEventInterface = mockk(relaxed = true) + + private lateinit var repository: SyllabusRepository + + @Before + fun setup() { + repository = SyllabusRepository(plannerApi, calendarEventApi) + } + + @Test + fun `getPlannerItems returns success when API call succeeds`() = runTest { + val contextCodes = listOf("course_1") + val expectedItems = listOf( + PlannerItem( + courseId = 1L, + groupId = null, + userId = null, + contextType = "Course", + contextName = "Test Course", + plannableType = PlannableType.QUIZ, + plannable = Plannable( + id = 123, + title = "Test Quiz", + courseId = 1L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = null, + assignmentId = null, + todoDate = Date().toApiString(), + startAt = null, + endAt = null, + details = null, + allDay = null + ), + plannableDate = Date(), + htmlUrl = null, + submissionState = null, + newActivity = false + ) + ) + + coEvery { + plannerApi.getPlannerItems(any(), any(), any(), any(), any()) + } returns DataResult.Success(expectedItems) + + val result = repository.getPlannerItems( + startDate = null, + endDate = null, + contextCodes = contextCodes, + filter = "all_ungraded_todo_items", + forceNetwork = false + ) + + assertTrue(result is DataResult.Success) + assertEquals(expectedItems, result.dataOrNull) + coVerify(exactly = 1) { + plannerApi.getPlannerItems( + null, + null, + contextCodes, + "all_ungraded_todo_items", + any() + ) + } + } + + @Test + fun `getPlannerItems returns failure when API call fails`() = runTest { + val contextCodes = listOf("course_1") + + coEvery { + plannerApi.getPlannerItems(any(), any(), any(), any(), any()) + } returns DataResult.Fail() + + val result = repository.getPlannerItems( + startDate = null, + endDate = null, + contextCodes = contextCodes, + filter = "all_ungraded_todo_items", + forceNetwork = true + ) + + assertTrue(result is DataResult.Fail) + coVerify(exactly = 1) { + plannerApi.getPlannerItems( + null, + null, + contextCodes, + "all_ungraded_todo_items", + any() + ) + } + } + + @Test + fun `getPlannerItems passes forceNetwork parameter correctly`() = runTest { + val contextCodes = listOf("course_1") + + coEvery { + plannerApi.getPlannerItems(any(), any(), any(), any(), any()) + } returns DataResult.Success(emptyList()) + + repository.getPlannerItems( + startDate = "2025-01-01", + endDate = "2025-01-31", + contextCodes = contextCodes, + filter = null, + forceNetwork = true + ) + + coVerify(exactly = 1) { + plannerApi.getPlannerItems( + "2025-01-01", + "2025-01-31", + contextCodes, + null, + match { it.isForceReadFromNetwork } + ) + } + } + + @Test + fun `getCalendarEvents returns success for ASSIGNMENT type when API call succeeds`() = runTest { + val contextCodes = listOf("course_1") + val expectedItems = listOf( + ScheduleItem( + itemId = "123", + title = "Test Assignment", + itemType = ScheduleItem.Type.TYPE_ASSIGNMENT, + contextCode = "course_1" + ) + ) + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(expectedItems) + + val result = repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.ASSIGNMENT, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = false + ) + + assertTrue(result is DataResult.Success) + assertEquals(expectedItems, result.dataOrNull) + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName, + null, + null, + contextCodes, + any() + ) + } + } + + @Test + fun `getCalendarEvents returns success for CALENDAR type when API call succeeds`() = runTest { + val contextCodes = listOf("course_1") + val expectedItems = listOf( + ScheduleItem( + itemId = "456", + title = "Test Calendar Event", + itemType = ScheduleItem.Type.TYPE_CALENDAR, + contextCode = "course_1" + ) + ) + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(expectedItems) + + val result = repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.CALENDAR, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = false + ) + + assertTrue(result is DataResult.Success) + assertEquals(expectedItems, result.dataOrNull) + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.CALENDAR.apiName, + null, + null, + contextCodes, + any() + ) + } + } + + @Test + fun `getCalendarEvents returns failure when API call fails`() = runTest { + val contextCodes = listOf("course_1") + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Fail() + + val result = repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.ASSIGNMENT, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = true + ) + + assertTrue(result is DataResult.Fail) + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName, + null, + null, + contextCodes, + any() + ) + } + } + + @Test + fun `getCalendarEvents passes date range parameters correctly`() = runTest { + val contextCodes = listOf("course_1", "course_2") + val startDate = "2025-01-01" + val endDate = "2025-01-31" + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(emptyList()) + + repository.getCalendarEvents( + allEvents = false, + type = CalendarEventAPI.CalendarEventType.CALENDAR, + startDate = startDate, + endDate = endDate, + contextCodes = contextCodes, + forceNetwork = false + ) + + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + false, + CalendarEventAPI.CalendarEventType.CALENDAR.apiName, + startDate, + endDate, + contextCodes, + match { !it.isForceReadFromNetwork } + ) + } + } + + @Test + fun `getCalendarEvents passes forceNetwork parameter correctly`() = runTest { + val contextCodes = listOf("course_1") + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(emptyList()) + + repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.ASSIGNMENT, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = true + ) + + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName, + null, + null, + contextCodes, + match { it.isForceReadFromNetwork } + ) + } + } + + @Test + fun `getCalendarEvents converts enum to apiName correctly`() = runTest { + val contextCodes = listOf("course_1") + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(emptyList()) + + // Test ASSIGNMENT type + repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.ASSIGNMENT, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = false + ) + + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + any(), + "assignment", + any(), + any(), + any(), + any() + ) + } + + // Test CALENDAR type + repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.CALENDAR, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = false + ) + + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + any(), + "event", + any(), + any(), + any(), + any() + ) + } + } + + @Test + fun `getCalendarEvents handles multiple context codes correctly`() = runTest { + val contextCodes = listOf("course_1", "course_2", "course_3") + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(emptyList()) + + repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.ASSIGNMENT, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = false + ) + + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + any(), + any(), + any(), + any(), + contextCodes, + any() + ) + } + } +} diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt index 3d3121b637..6d6e8c236a 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt @@ -290,6 +290,29 @@ object ApiEndpoint : Endpoint( val plannerOverride = getJsonFromRequestBody(request.body) request.successResponse(plannerOverride!!) } + }, + Segment("items") to Endpoint { + GET { + val contextCodes = request.url.queryParameterValues("context_codes[]") + val startDate = request.url.queryParameter("start_date").toDate() + val endDate = request.url.queryParameter("end_date").toDate() + val filter = request.url.queryParameter("filter") + val courseIds = contextCodes + .filter { it?.startsWith("course_").orDefault() } + .map { it?.substringAfter("course_")?.toLong() } + val userIds = contextCodes + .filter { it?.startsWith("user_").orDefault() } + .map { it?.substringAfter("user_")?.toLong() } + + val plannerItems = data.todos.filter { + courseIds.contains(it.courseId) || userIds.contains(it.userId) + }.filter { + if (it.plannableDate == null) return@filter true + if (startDate == null || endDate == null) return@filter true + it.plannableDate.time in startDate.time..endDate.time + } + request.successResponse(plannerItems) + } } ), Segment("features") to Endpoint( diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt index 202e2e8ba2..6b17dcbb2d 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt @@ -33,6 +33,7 @@ object PlannerAPI { @Query("start_date") startDate: String?, @Query("end_date") endDate: String?, @Query(value = "context_codes[]", encoded = true) contextCodes: List, + @Query("filter") filter: String? = null, @Tag restParams: RestParams ): DataResult> diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt index 57f9b4a226..a68ac52c95 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt @@ -17,6 +17,7 @@ package com.instructure.canvasapi2.models import android.os.Parcelable import com.google.gson.annotations.SerializedName +import com.instructure.canvasapi2.utils.toApiString import kotlinx.parcelize.Parcelize import java.util.* @@ -81,6 +82,34 @@ data class PlannerItem ( override val comparisonString: String get() = "${plannable.title}${plannable.subAssignmentTag}" + fun toScheduleItem(): ScheduleItem { + val contextCode = when { + courseId != null -> "course_$courseId" + groupId != null -> "group_$groupId" + userId != null -> "user_$userId" + else -> null + } + + return ScheduleItem( + itemId = plannable.id.toString(), + title = plannable.title, + description = plannable.details, + startAt = plannable.todoDate ?: plannableDate.toApiString(), + endAt = plannable.endAt?.toApiString(), + isAllDay = plannable.allDay ?: false, + allDayAt = if (plannable.allDay == true) plannable.todoDate else null, + htmlUrl = htmlUrl, + contextCode = contextCode, + contextName = contextName, + type = when (plannableType) { + PlannableType.DISCUSSION_TOPIC -> "discussion_topic" + PlannableType.WIKI_PAGE -> "wiki_page" + else -> plannableType.name.lowercase() + }, + itemType = ScheduleItem.Type.TYPE_SYLLABUS + ) + } + } enum class PlannableType { diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/7.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/7.json new file mode 100644 index 0000000000..435ec0fe7d --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/7.json @@ -0,0 +1,6002 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "452f8a3a37230a66a3dafed1956528e6", + "entities": [ + { + "tableName": "AssignmentDueDateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `assignmentOverrideId` INTEGER, `dueAt` TEXT, `title` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `isBase` INTEGER NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isBase", + "columnName": "isBase", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `submissionTypesRaw` TEXT NOT NULL, `dueAt` TEXT, `pointsPossible` REAL NOT NULL, `courseId` INTEGER NOT NULL, `isGradeGroupsIndividually` INTEGER NOT NULL, `gradingType` TEXT, `needsGradingCount` INTEGER NOT NULL, `htmlUrl` TEXT, `url` TEXT, `quizId` INTEGER NOT NULL, `isUseRubricForGrading` INTEGER NOT NULL, `rubricSettingsId` INTEGER, `allowedExtensions` TEXT NOT NULL, `submissionId` INTEGER, `assignmentGroupId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `isPeerReviews` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `lockExplanation` TEXT, `discussionTopicHeaderId` INTEGER, `freeFormCriterionComments` INTEGER NOT NULL, `published` INTEGER NOT NULL, `groupCategoryId` INTEGER NOT NULL, `userSubmitted` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `onlyVisibleToOverrides` INTEGER NOT NULL, `anonymousPeerReviews` INTEGER NOT NULL, `moderatedGrading` INTEGER NOT NULL, `anonymousGrading` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `plannerOverrideId` INTEGER, `isStudioEnabled` INTEGER NOT NULL, `inClosedGradingPeriod` INTEGER NOT NULL, `annotatableAttachmentId` INTEGER NOT NULL, `anonymousSubmissions` INTEGER NOT NULL, `omitFromFinalGrade` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentGroupId`) REFERENCES `AssignmentGroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionTypesRaw", + "columnName": "submissionTypesRaw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeGroupsIndividually", + "columnName": "isGradeGroupsIndividually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingType", + "columnName": "gradingType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUseRubricForGrading", + "columnName": "isUseRubricForGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricSettingsId", + "columnName": "rubricSettingsId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "allowedExtensions", + "columnName": "allowedExtensions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPeerReviews", + "columnName": "isPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSubmitted", + "columnName": "userSubmitted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousPeerReviews", + "columnName": "anonymousPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moderatedGrading", + "columnName": "moderatedGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousGrading", + "columnName": "anonymousGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isStudioEnabled", + "columnName": "isStudioEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inClosedGradingPeriod", + "columnName": "inClosedGradingPeriod", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "annotatableAttachmentId", + "columnName": "annotatableAttachmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousSubmissions", + "columnName": "anonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "omitFromFinalGrade", + "columnName": "omitFromFinalGrade", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentGroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentGroupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `position` INTEGER NOT NULL, `groupWeight` REAL NOT NULL, `rules` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupWeight", + "columnName": "groupWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `title` TEXT, `dueAt` INTEGER, `isAllDay` INTEGER NOT NULL, `allDayDate` TEXT, `unlockAt` INTEGER, `lockAt` INTEGER, `courseSectionId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayDate", + "columnName": "allDayDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentRubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `rubricId` TEXT NOT NULL, PRIMARY KEY(`assignmentId`, `rubricId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricId", + "columnName": "rubricId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId", + "rubricId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentScoreStatisticsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `mean` REAL NOT NULL, `min` REAL NOT NULL, `max` REAL NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mean", + "columnName": "mean", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "min", + "columnName": "min", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "max", + "columnName": "max", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentSetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `scoringRangeId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `position` INTEGER NOT NULL, `masteryPathId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`masteryPathId`) REFERENCES `MasteryPathEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringRangeId", + "columnName": "scoringRangeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "masteryPathId", + "columnName": "masteryPathId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "MasteryPathEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "masteryPathId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `originalName` TEXT, `courseCode` TEXT, `startAt` TEXT, `endAt` TEXT, `syllabusBody` TEXT, `hideFinalGrades` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `license` TEXT NOT NULL, `termId` INTEGER, `needsGradingCount` INTEGER NOT NULL, `isApplyAssignmentGroupWeights` INTEGER NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, `isFavorite` INTEGER NOT NULL, `accessRestrictedByDate` INTEGER NOT NULL, `imageUrl` TEXT, `bannerImageUrl` TEXT, `isWeightedGradingPeriods` INTEGER NOT NULL, `hasGradingPeriods` INTEGER NOT NULL, `homePage` TEXT, `restrictEnrollmentsToCourseDate` INTEGER NOT NULL, `workflowState` TEXT, `homeroomCourse` INTEGER NOT NULL, `courseColor` TEXT, `gradingScheme` TEXT, `pointsBasedGradingScheme` INTEGER NOT NULL, `scalingFactor` REAL NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`termId`) REFERENCES `TermEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syllabusBody", + "columnName": "syllabusBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideFinalGrades", + "columnName": "hideFinalGrades", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "termId", + "columnName": "termId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isApplyAssignmentGroupWeights", + "columnName": "isApplyAssignmentGroupWeights", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessRestrictedByDate", + "columnName": "accessRestrictedByDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bannerImageUrl", + "columnName": "bannerImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isWeightedGradingPeriods", + "columnName": "isWeightedGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasGradingPeriods", + "columnName": "hasGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homePage", + "columnName": "homePage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "restrictEnrollmentsToCourseDate", + "columnName": "restrictEnrollmentsToCourseDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeroomCourse", + "columnName": "homeroomCourse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseColor", + "columnName": "courseColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "gradingScheme", + "columnName": "gradingScheme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsBasedGradingScheme", + "columnName": "pointsBasedGradingScheme", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scalingFactor", + "columnName": "scalingFactor", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "TermEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "termId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFilesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`courseId`, `url`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "url" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "CourseGradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `gradingPeriodId` INTEGER NOT NULL, PRIMARY KEY(`courseId`, `gradingPeriodId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`gradingPeriodId`) REFERENCES `GradingPeriodEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "gradingPeriodId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "GradingPeriodEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "gradingPeriodId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseSummary` INTEGER, `restrictQuantitativeData` INTEGER NOT NULL, PRIMARY KEY(`courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseSummary", + "columnName": "courseSummary", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "restrictQuantitativeData", + "columnName": "restrictQuantitativeData", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `fullContentSync` INTEGER NOT NULL, `tabs` TEXT NOT NULL, `fullFileSync` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullContentSync", + "columnName": "fullContentSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullFileSync", + "columnName": "fullFileSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DashboardCardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isK5Subject` INTEGER NOT NULL, `shortName` TEXT, `originalName` TEXT, `courseCode` TEXT, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isK5Subject", + "columnName": "isK5Subject", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionEntryAttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionEntryId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionEntryId`, `remoteFileId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionEntryId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` TEXT, `createdAt` TEXT, `authorId` INTEGER, `description` TEXT, `userId` INTEGER NOT NULL, `parentId` INTEGER NOT NULL, `message` TEXT, `deleted` INTEGER NOT NULL, `totalChildren` INTEGER NOT NULL, `unreadChildren` INTEGER NOT NULL, `ratingCount` INTEGER NOT NULL, `ratingSum` INTEGER NOT NULL, `editorId` INTEGER NOT NULL, `_hasRated` INTEGER NOT NULL, `replyIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChildren", + "columnName": "totalChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadChildren", + "columnName": "unreadChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingCount", + "columnName": "ratingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingSum", + "columnName": "ratingSum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editorId", + "columnName": "editorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_hasRated", + "columnName": "_hasRated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyIds", + "columnName": "replyIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionParticipantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `pronouns` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionTopicHeaderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `discussionType` TEXT, `title` TEXT, `message` TEXT, `htmlUrl` TEXT, `postedDate` INTEGER, `delayedPostDate` INTEGER, `lastReplyDate` INTEGER, `requireInitialPost` INTEGER NOT NULL, `discussionSubentryCount` INTEGER NOT NULL, `readState` TEXT, `unreadCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `assignmentId` INTEGER, `locked` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `pinned` INTEGER NOT NULL, `authorId` INTEGER, `podcastUrl` TEXT, `groupCategoryId` TEXT, `announcement` INTEGER NOT NULL, `permissionId` INTEGER, `published` INTEGER NOT NULL, `allowRating` INTEGER NOT NULL, `onlyGradersCanRate` INTEGER NOT NULL, `sortByRating` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `lockAt` INTEGER, `userCanSeePosts` INTEGER NOT NULL, `specificSections` TEXT, `anonymousState` TEXT, `replyRequiredCount` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`authorId`) REFERENCES `DiscussionParticipantEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`permissionId`) REFERENCES `DiscussionTopicPermissionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionType", + "columnName": "discussionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedDate", + "columnName": "postedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "delayedPostDate", + "columnName": "delayedPostDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastReplyDate", + "columnName": "lastReplyDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "requireInitialPost", + "columnName": "requireInitialPost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionSubentryCount", + "columnName": "discussionSubentryCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readState", + "columnName": "readState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unreadCount", + "columnName": "unreadCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "podcastUrl", + "columnName": "podcastUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "announcement", + "columnName": "announcement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissionId", + "columnName": "permissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowRating", + "columnName": "allowRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyGradersCanRate", + "columnName": "onlyGradersCanRate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortByRating", + "columnName": "sortByRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userCanSeePosts", + "columnName": "userCanSeePosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "specificSections", + "columnName": "specificSections", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "anonymousState", + "columnName": "anonymousState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyRequiredCount", + "columnName": "replyRequiredCount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionParticipantEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "authorId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "DiscussionTopicPermissionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "permissionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `discussionTopicHeaderId` INTEGER NOT NULL, `attach` INTEGER NOT NULL, `update` INTEGER NOT NULL, `delete` INTEGER NOT NULL, `reply` INTEGER NOT NULL, FOREIGN KEY(`discussionTopicHeaderId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attach", + "columnName": "attach", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delete", + "columnName": "delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicHeaderId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicRemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionId`, `remoteFileId`), FOREIGN KEY(`discussionId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionId", + "columnName": "discussionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicSectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionTopicId` INTEGER NOT NULL, `sectionId` INTEGER NOT NULL, PRIMARY KEY(`discussionTopicId`, `sectionId`), FOREIGN KEY(`discussionTopicId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionTopicId", + "columnName": "discussionTopicId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionTopicId", + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `role` TEXT NOT NULL, `type` TEXT NOT NULL, `courseId` INTEGER, `courseSectionId` INTEGER, `enrollmentState` TEXT, `userId` INTEGER NOT NULL, `computedCurrentScore` REAL, `computedFinalScore` REAL, `computedCurrentGrade` TEXT, `computedFinalGrade` TEXT, `multipleGradingPeriodsEnabled` INTEGER NOT NULL, `totalsForAllGradingPeriodsOption` INTEGER NOT NULL, `currentPeriodComputedCurrentScore` REAL, `currentPeriodComputedFinalScore` REAL, `currentPeriodComputedCurrentGrade` TEXT, `currentPeriodComputedFinalGrade` TEXT, `currentGradingPeriodId` INTEGER NOT NULL, `currentGradingPeriodTitle` TEXT, `associatedUserId` INTEGER NOT NULL, `lastActivityAt` INTEGER, `limitPrivilegesToCourseSection` INTEGER NOT NULL, `observedUserId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`observedUserId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseSectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "computedCurrentScore", + "columnName": "computedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedFinalScore", + "columnName": "computedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedCurrentGrade", + "columnName": "computedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "computedFinalGrade", + "columnName": "computedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "multipleGradingPeriodsEnabled", + "columnName": "multipleGradingPeriodsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalsForAllGradingPeriodsOption", + "columnName": "totalsForAllGradingPeriodsOption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPeriodComputedCurrentScore", + "columnName": "currentPeriodComputedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalScore", + "columnName": "currentPeriodComputedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedCurrentGrade", + "columnName": "currentPeriodComputedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalGrade", + "columnName": "currentPeriodComputedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentGradingPeriodId", + "columnName": "currentGradingPeriodId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentGradingPeriodTitle", + "columnName": "currentGradingPeriodTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "associatedUserId", + "columnName": "associatedUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivityAt", + "columnName": "lastActivityAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "limitPrivilegesToCourseSection", + "columnName": "limitPrivilegesToCourseSection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "observedUserId", + "columnName": "observedUserId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "observedUserId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "courseSectionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileFolderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `createdDate` INTEGER, `updatedDate` INTEGER, `unlockDate` INTEGER, `lockDate` INTEGER, `isLocked` INTEGER NOT NULL, `isHidden` INTEGER NOT NULL, `isLockedForUser` INTEGER NOT NULL, `isHiddenForUser` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `size` INTEGER NOT NULL, `contentType` TEXT, `url` TEXT, `displayName` TEXT, `thumbnailUrl` TEXT, `parentFolderId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `filesCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `foldersCount` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `foldersUrl` TEXT, `filesUrl` TEXT, `fullName` TEXT, `forSubmissions` INTEGER NOT NULL, `canUpload` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedDate", + "columnName": "updatedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unlockDate", + "columnName": "unlockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockDate", + "columnName": "lockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLockedForUser", + "columnName": "isLockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHiddenForUser", + "columnName": "isHiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filesCount", + "columnName": "filesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "foldersCount", + "columnName": "foldersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "foldersUrl", + "columnName": "foldersUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesUrl", + "columnName": "filesUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "forSubmissions", + "columnName": "forSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canUpload", + "columnName": "canUpload", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EditDashboardItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `name` TEXT NOT NULL, `isFavorite` INTEGER NOT NULL, `enrollmentState` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ExternalToolAttributesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `url` TEXT, `newTab` INTEGER NOT NULL, `resourceLinkid` TEXT, `contentId` INTEGER, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "newTab", + "columnName": "newTab", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceLinkid", + "columnName": "resourceLinkid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`enrollmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, PRIMARY KEY(`enrollmentId`), FOREIGN KEY(`enrollmentId`) REFERENCES `EnrollmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "enrollmentId", + "columnName": "enrollmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "enrollmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "EnrollmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "enrollmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `startDate` TEXT, `endDate` TEXT, `weight` REAL NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `avatarUrl` TEXT, `isPublic` INTEGER NOT NULL, `membersCount` INTEGER NOT NULL, `joinLevel` TEXT, `courseId` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `role` TEXT, `groupCategoryId` INTEGER NOT NULL, `storageQuotaMb` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `concluded` INTEGER NOT NULL, `canAccess` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinLevel", + "columnName": "joinLevel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "storageQuotaMb", + "columnName": "storageQuotaMb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concluded", + "columnName": "concluded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canAccess", + "columnName": "canAccess", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupUserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LocalFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `createdDate` INTEGER NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MasteryPathAssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `overrideId` INTEGER NOT NULL, `assignmentSetId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentSetId`) REFERENCES `AssignmentSetEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "overrideId", + "columnName": "overrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentSetId", + "columnName": "assignmentSetId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentSetEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MasteryPathEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `selectedSetId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedSetId", + "columnName": "selectedSetId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleContentDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pointsPossible` TEXT, `dueAt` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `moduleId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `title` TEXT, `indent` INTEGER NOT NULL, `type` TEXT, `htmlUrl` TEXT, `url` TEXT, `published` INTEGER, `contentId` INTEGER NOT NULL, `externalUrl` TEXT, `pageUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`moduleId`) REFERENCES `ModuleObjectEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "indent", + "columnName": "indent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageUrl", + "columnName": "pageUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleObjectEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleObjectEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `name` TEXT, `unlockAt` TEXT, `sequentialProgress` INTEGER NOT NULL, `prerequisiteIds` TEXT, `state` TEXT, `completedAt` TEXT, `published` INTEGER, `itemCount` INTEGER NOT NULL, `itemsUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sequentialProgress", + "columnName": "sequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prerequisiteIds", + "columnName": "prerequisiteIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "completedAt", + "columnName": "completedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemCount", + "columnName": "itemCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemsUrl", + "columnName": "itemsUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "NeedsGradingCountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sectionId` INTEGER NOT NULL, `needsGradingCount` INTEGER NOT NULL, PRIMARY KEY(`sectionId`), FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `createdAt` INTEGER, `updatedAt` INTEGER, `hideFromStudents` INTEGER NOT NULL, `status` TEXT, `body` TEXT, `frontPage` INTEGER NOT NULL, `published` INTEGER NOT NULL, `editingRoles` TEXT, `htmlUrl` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hideFromStudents", + "columnName": "hideFromStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "frontPage", + "columnName": "frontPage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editingRoles", + "columnName": "editingRoles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER, `groupId` INTEGER, `userId` INTEGER, `contextType` TEXT, `contextName` TEXT, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `plannableTitle` TEXT, `plannableDetails` TEXT, `plannableTodoDate` TEXT, `plannableEndAt` INTEGER, `plannableAllDay` INTEGER, `plannableCourseId` INTEGER, `plannableGroupId` INTEGER, `plannableUserId` INTEGER, `plannableDate` INTEGER NOT NULL, `htmlUrl` TEXT, `submissionStateSubmitted` INTEGER, `submissionStateExcused` INTEGER, `submissionStateGraded` INTEGER, `newActivity` INTEGER, `plannerOverrideId` INTEGER, `plannerOverrideMarkedComplete` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextName", + "columnName": "contextName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableTitle", + "columnName": "plannableTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableDetails", + "columnName": "plannableDetails", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableTodoDate", + "columnName": "plannableTodoDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableEndAt", + "columnName": "plannableEndAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableAllDay", + "columnName": "plannableAllDay", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableCourseId", + "columnName": "plannableCourseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableGroupId", + "columnName": "plannableGroupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableUserId", + "columnName": "plannableUserId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableDate", + "columnName": "plannableDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionStateSubmitted", + "columnName": "submissionStateSubmitted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionStateExcused", + "columnName": "submissionStateExcused", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionStateGraded", + "columnName": "submissionStateGraded", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "newActivity", + "columnName": "newActivity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannerOverrideMarkedComplete", + "columnName": "plannerOverrideMarkedComplete", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, `markedComplete` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "markedComplete", + "columnName": "markedComplete", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `displayName` TEXT, `fileName` TEXT, `contentType` TEXT, `url` TEXT, `size` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `unlockAt` TEXT, `locked` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `lockAt` TEXT, `hiddenForUser` INTEGER NOT NULL, `thumbnailUrl` TEXT, `modifiedAt` TEXT, `lockedForUser` INTEGER NOT NULL, `previewUrl` TEXT, `lockExplanation` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hiddenForUser", + "columnName": "hiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "modifiedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RubricCriterionAssessmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `assignmentId` INTEGER NOT NULL, `ratingId` TEXT, `points` REAL, `comments` TEXT, PRIMARY KEY(`id`, `assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingId", + "columnName": "ratingId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "comments", + "columnName": "comments", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `criterionUseRange` INTEGER NOT NULL, `ignoreForScoring` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "criterionUseRange", + "columnName": "criterionUseRange", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoreForScoring", + "columnName": "ignoreForScoring", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionRatingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `rubricCriterionId` TEXT NOT NULL, PRIMARY KEY(`id`, `rubricCriterionId`), FOREIGN KEY(`rubricCriterionId`) REFERENCES `RubricCriterionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rubricCriterionId", + "columnName": "rubricCriterionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "rubricCriterionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "RubricCriterionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "rubricCriterionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `pointsPossible` REAL NOT NULL, `title` TEXT NOT NULL, `isReusable` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `isReadOnly` INTEGER NOT NULL, `freeFormCriterionComments` INTEGER NOT NULL, `hideScoreTotal` INTEGER NOT NULL, `hidePoints` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isReusable", + "columnName": "isReusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isReadOnly", + "columnName": "isReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideScoreTotal", + "columnName": "hideScoreTotal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidePoints", + "columnName": "hidePoints", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemAssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentOverrideId` INTEGER NOT NULL, `scheduleItemId` TEXT NOT NULL, PRIMARY KEY(`assignmentOverrideId`, `scheduleItemId`), FOREIGN KEY(`assignmentOverrideId`) REFERENCES `AssignmentOverrideEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`scheduleItemId`) REFERENCES `ScheduleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduleItemId", + "columnName": "scheduleItemId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentOverrideId", + "scheduleItemId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentOverrideEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentOverrideId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ScheduleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "scheduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `description` TEXT, `startAt` TEXT, `endAt` TEXT, `isAllDay` INTEGER NOT NULL, `allDayAt` TEXT, `locationAddress` TEXT, `locationName` TEXT, `htmlUrl` TEXT, `contextCode` TEXT, `effectiveContextCode` TEXT, `isHidden` INTEGER NOT NULL, `importantDates` INTEGER NOT NULL, `assignmentId` INTEGER, `type` TEXT NOT NULL, `itemType` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayAt", + "columnName": "allDayAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationAddress", + "columnName": "locationAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationName", + "columnName": "locationName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextCode", + "columnName": "contextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effectiveContextCode", + "columnName": "effectiveContextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "importantDates", + "columnName": "importantDates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemType", + "columnName": "itemType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER, `startAt` TEXT, `endAt` TEXT, `totalStudents` INTEGER NOT NULL, `restrictEnrollmentsToSectionDates` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "totalStudents", + "columnName": "totalStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "restrictEnrollmentsToSectionDates", + "columnName": "restrictEnrollmentsToSectionDates", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionDiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`submissionId` INTEGER NOT NULL, `discussionEntryId` INTEGER NOT NULL, PRIMARY KEY(`submissionId`, `discussionEntryId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "submissionId", + "discussionEntryId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `attempt` INTEGER NOT NULL, `submittedAt` INTEGER, `commentCreated` INTEGER, `mediaContentType` TEXT, `mediaCommentUrl` TEXT, `mediaCommentDisplay` TEXT, `body` TEXT, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, `workflowState` TEXT, `submissionType` TEXT, `previewUrl` TEXT, `url` TEXT, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `mediaCommentId` TEXT, `assignmentId` INTEGER NOT NULL, `userId` INTEGER, `graderId` INTEGER, `groupId` INTEGER, `pointsDeducted` REAL, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `postedAt` INTEGER, `gradingPeriodId` INTEGER, `customGradeStatusId` INTEGER, `hasSubAssignmentSubmissions` INTEGER NOT NULL, PRIMARY KEY(`id`, `attempt`), FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submittedAt", + "columnName": "submittedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "commentCreated", + "columnName": "commentCreated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaContentType", + "columnName": "mediaContentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentUrl", + "columnName": "mediaCommentUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentDisplay", + "columnName": "mediaCommentDisplay", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionType", + "columnName": "submissionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "graderId", + "columnName": "graderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pointsDeducted", + "columnName": "pointsDeducted", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasSubAssignmentSubmissions", + "columnName": "hasSubAssignmentSubmissions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "attempt" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `autoSyncEnabled` INTEGER NOT NULL, `syncFrequency` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoSyncEnabled", + "columnName": "autoSyncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncFrequency", + "columnName": "syncFrequency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifiOnly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TabEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `label` TEXT, `type` TEXT NOT NULL, `htmlUrl` TEXT, `externalUrl` TEXT, `visibility` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL, `ltiUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ltiUrl", + "columnName": "ltiUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "TermEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `startAt` TEXT, `endAt` TEXT, `isGroupTerm` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGroupTerm", + "columnName": "isGroupTerm", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserCalendarEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ics` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ics", + "columnName": "ics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `shortName` TEXT, `loginId` TEXT, `avatarUrl` TEXT, `primaryEmail` TEXT, `email` TEXT, `sortableName` TEXT, `bio` TEXT, `enrollmentIndex` INTEGER NOT NULL, `lastLogin` TEXT, `locale` TEXT, `effective_locale` TEXT, `pronouns` TEXT, `k5User` INTEGER NOT NULL, `rootAccount` TEXT, `isFakeStudent` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginId", + "columnName": "loginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "primaryEmail", + "columnName": "primaryEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortableName", + "columnName": "sortableName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bio", + "columnName": "bio", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enrollmentIndex", + "columnName": "enrollmentIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastLogin", + "columnName": "lastLogin", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effective_locale", + "columnName": "effective_locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "k5User", + "columnName": "k5User", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rootAccount", + "columnName": "rootAccount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFakeStudent", + "columnName": "isFakeStudent", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "QuizEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `mobileUrl` TEXT, `htmlUrl` TEXT, `description` TEXT, `quizType` TEXT, `assignmentGroupId` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `questionCount` INTEGER NOT NULL, `pointsPossible` TEXT, `isLockQuestionsAfterAnswering` INTEGER NOT NULL, `dueAt` TEXT, `timeLimit` INTEGER NOT NULL, `shuffleAnswers` INTEGER NOT NULL, `showCorrectAnswers` INTEGER NOT NULL, `scoringPolicy` TEXT, `accessCode` TEXT, `ipFilter` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `hideResults` TEXT, `showCorrectAnswersAt` TEXT, `hideCorrectAnswersAt` TEXT, `unlockAt` TEXT, `oneTimeResults` INTEGER NOT NULL, `lockAt` TEXT, `questionTypes` TEXT NOT NULL, `hasAccessCode` INTEGER NOT NULL, `oneQuestionAtATime` INTEGER NOT NULL, `requireLockdownBrowser` INTEGER NOT NULL, `requireLockdownBrowserForResults` INTEGER NOT NULL, `allowAnonymousSubmissions` INTEGER NOT NULL, `published` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `isOnlyVisibleToOverrides` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mobileUrl", + "columnName": "mobileUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizType", + "columnName": "quizType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionCount", + "columnName": "questionCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLockQuestionsAfterAnswering", + "columnName": "isLockQuestionsAfterAnswering", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeLimit", + "columnName": "timeLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shuffleAnswers", + "columnName": "shuffleAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showCorrectAnswers", + "columnName": "showCorrectAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringPolicy", + "columnName": "scoringPolicy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accessCode", + "columnName": "accessCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ipFilter", + "columnName": "ipFilter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideResults", + "columnName": "hideResults", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showCorrectAnswersAt", + "columnName": "showCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideCorrectAnswersAt", + "columnName": "hideCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oneTimeResults", + "columnName": "oneTimeResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "questionTypes", + "columnName": "questionTypes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasAccessCode", + "columnName": "hasAccessCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneQuestionAtATime", + "columnName": "oneQuestionAtATime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowser", + "columnName": "requireLockdownBrowser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowserForResults", + "columnName": "requireLockdownBrowserForResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowAnonymousSubmissions", + "columnName": "allowAnonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnlyVisibleToOverrides", + "columnName": "isOnlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockInfoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `modulePrerequisiteNames` TEXT, `unlockAt` TEXT, `lockedModuleId` INTEGER, `assignmentId` INTEGER, `moduleId` INTEGER, `pageId` INTEGER, FOREIGN KEY(`moduleId`) REFERENCES `ModuleContentDetailsEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`pageId`) REFERENCES `PageEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modulePrerequisiteNames", + "columnName": "modulePrerequisiteNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleContentDetailsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "PageEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockedModuleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `unlockAt` TEXT, `isRequireSequentialProgress` INTEGER NOT NULL, `lockInfoId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`lockInfoId`) REFERENCES `LockInfoEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isRequireSequentialProgress", + "columnName": "isRequireSequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockInfoId", + "columnName": "lockInfoId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockInfoEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockInfoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleNameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `lockedModuleId` INTEGER NOT NULL, FOREIGN KEY(`lockedModuleId`) REFERENCES `LockedModuleEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockedModuleEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockedModuleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleCompletionRequirementEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT, `minScore` REAL NOT NULL, `maxScore` REAL NOT NULL, `completed` INTEGER, `moduleId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "minScore", + "columnName": "minScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxScore", + "columnName": "maxScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `fileName` TEXT, `courseId` INTEGER NOT NULL, `url` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "ConferenceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `conferenceKey` TEXT, `conferenceType` TEXT, `description` TEXT, `duration` INTEGER NOT NULL, `endedAt` INTEGER, `hasAdvancedSettings` INTEGER NOT NULL, `joinUrl` TEXT, `longRunning` INTEGER NOT NULL, `startedAt` INTEGER, `title` TEXT, `url` TEXT, `contextType` TEXT NOT NULL, `contextId` INTEGER NOT NULL, `record` INTEGER, `users` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conferenceKey", + "columnName": "conferenceKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conferenceType", + "columnName": "conferenceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAdvancedSettings", + "columnName": "hasAdvancedSettings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinUrl", + "columnName": "joinUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longRunning", + "columnName": "longRunning", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startedAt", + "columnName": "startedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "record", + "columnName": "record", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "users", + "columnName": "users", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ConferenceRecordingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recordingId` TEXT NOT NULL, `conferenceId` INTEGER NOT NULL, `createdAtMillis` INTEGER NOT NULL, `durationMinutes` INTEGER NOT NULL, `playbackUrl` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`recordingId`), FOREIGN KEY(`conferenceId`) REFERENCES `ConferenceEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "recordingId", + "columnName": "recordingId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conferenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtMillis", + "columnName": "createdAtMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durationMinutes", + "columnName": "durationMinutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playbackUrl", + "columnName": "playbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recordingId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ConferenceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "conferenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFeaturesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `features` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionCommentId`) REFERENCES `SubmissionCommentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionCommentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionCommentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `submissionId` INTEGER NOT NULL, `attemptId` INTEGER NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "DiscussionTopicEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unreadEntries` TEXT NOT NULL, `participantIds` TEXT NOT NULL, `viewIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadEntries", + "columnName": "unreadEntries", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantIds", + "columnName": "participantIds", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewIds", + "columnName": "viewIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CourseSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `tabs` TEXT NOT NULL, `additionalFilesStarted` INTEGER NOT NULL, `progressState` TEXT NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalFilesStarted", + "columnName": "additionalFilesStarted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalFile", + "columnName": "additionalFile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncProgressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "StudioMediaProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ltiLaunchId` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "ltiLaunchId", + "columnName": "ltiLaunchId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomGradeStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CheckpointEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `assignmentId` INTEGER NOT NULL, `name` TEXT, `tag` TEXT, `pointsPossible` REAL, `dueAt` TEXT, `onlyVisibleToOverrides` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubAssignmentSubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `submissionId` INTEGER NOT NULL, `submissionAttempt` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `latePolicyStatus` TEXT, `customGradeStatusId` INTEGER, `subAssignmentTag` TEXT, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `userId` INTEGER NOT NULL, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, FOREIGN KEY(`submissionId`, `submissionAttempt`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionAttempt", + "columnName": "submissionAttempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latePolicyStatus", + "columnName": "latePolicyStatus", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subAssignmentTag", + "columnName": "subAssignmentTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "submissionAttempt" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '452f8a3a37230a66a3dafed1956528e6')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PlannerItemDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PlannerItemDaoTest.kt new file mode 100644 index 0000000000..1a7d41f03c --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PlannerItemDaoTest.kt @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2025 - 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.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Plannable +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.PlannerItemEntity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class PlannerItemDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var plannerItemDao: PlannerItemDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + plannerItemDao = db.plannerItemDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindById() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + val plannerItems = createPlannerItems(listOf(1L, 2L), 1L) + val expectedPlannerItem = plannerItems[1] + + plannerItems.forEach { + plannerItemDao.insert(it) + } + + val result = plannerItemDao.findById(2L) + + assertEquals(expectedPlannerItem.plannableTitle, result?.plannableTitle) + } + + @Test + fun testFindByCourseId() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + courseDao.insert(CourseEntity(Course(2L))) + val plannerItems = listOf( + createPlannerItem(1L, 2L, "Item 1"), + createPlannerItem(2L, 1L, "Item 2"), + createPlannerItem(3L, 2L, "Item 3") + ) + val expectedItems = plannerItems.filter { it.courseId == 2L } + + plannerItems.forEach { plannerItemDao.insert(it) } + + val result = plannerItemDao.findByCourseId(2L) + assertEquals(expectedItems.map { it.plannableTitle }, result.map { it.plannableTitle }) + } + + @Test + fun testFindByCourseIds() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + courseDao.insert(CourseEntity(Course(2L))) + courseDao.insert(CourseEntity(Course(3L))) + val plannerItems = listOf( + createPlannerItem(1L, 1L, "Item 1"), + createPlannerItem(2L, 2L, "Item 2"), + createPlannerItem(3L, 3L, "Item 3"), + createPlannerItem(4L, 1L, "Item 4") + ) + val expectedItems = plannerItems.filter { it.courseId in listOf(1L, 2L) } + + plannerItems.forEach { plannerItemDao.insert(it) } + + val result = plannerItemDao.findByCourseIds(listOf(1L, 2L)) + assertEquals(expectedItems.map { it.plannableTitle }.sortedBy { it }, result.map { it.plannableTitle }.sortedBy { it }) + } + + @Test + fun testInsertReplace() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + val plannerItem1 = createPlannerItem(1L, 1L, "Item 1") + val plannerItem2 = createPlannerItem(1L, 1L, "Item 2") + + plannerItemDao.insert(plannerItem1) + plannerItemDao.insert(plannerItem2) + + val result = plannerItemDao.findById(1L) + + assertEquals(plannerItem2.plannableTitle, result?.plannableTitle) + } + + @Test(expected = SQLiteConstraintException::class) + fun testForeignKeyConstraint() = runTest { + plannerItemDao.insert(createPlannerItem(1L, 1L, "Item 1")) + } + + @Test + fun testInsertAll() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + + val plannerItems = createPlannerItems(listOf(1L, 2L, 3L), 1L) + plannerItemDao.insertAll(plannerItems) + + val result = plannerItemDao.findByCourseId(1L) + assertEquals(plannerItems.size, result.size) + } + + @Test + fun testDeleteAllByCourseId() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + courseDao.insert(CourseEntity(Course(2L))) + + val plannerItems = listOf( + createPlannerItem(1L, 1L, "Item 1"), + createPlannerItem(2L, 1L, "Item 2"), + createPlannerItem(3L, 2L, "Item 3") + ) + plannerItems.forEach { plannerItemDao.insert(it) } + + val resultBefore = plannerItemDao.findByCourseId(1L) + assertEquals(2, resultBefore.size) + + plannerItemDao.deleteAllByCourseId(1L) + + val resultAfter = plannerItemDao.findByCourseId(1L) + Assert.assertTrue(resultAfter.isEmpty()) + + val course2Items = plannerItemDao.findByCourseId(2L) + assertEquals(1, course2Items.size) + } + + @Test + fun testUpdate() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + val plannerItem = createPlannerItem(1L, 1L, "Item 1") + plannerItemDao.insert(plannerItem) + + val updatedItem = plannerItem.copy(plannableTitle = "Updated Item") + plannerItemDao.update(updatedItem) + + val result = plannerItemDao.findById(1L) + assertEquals("Updated Item", result?.plannableTitle) + } + + @Test + fun testDelete() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + val plannerItem = createPlannerItem(1L, 1L, "Item 1") + plannerItemDao.insert(plannerItem) + + val resultBefore = plannerItemDao.findById(1L) + assertEquals(plannerItem.plannableTitle, resultBefore?.plannableTitle) + + plannerItemDao.delete(plannerItem) + + val resultAfter = plannerItemDao.findById(1L) + assertEquals(null, resultAfter) + } + + private fun createPlannerItem(id: Long, courseId: Long, title: String): PlannerItemEntity { + val plannable = Plannable( + id = id, + title = title, + courseId = courseId, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = id, + todoDate = null, + startAt = null, + endAt = null, + details = "Details for $title", + allDay = false + ) + val plannerItem = PlannerItem( + courseId = courseId, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course $courseId", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable, + plannableDate = Date(), + htmlUrl = "https://example.com/item/$id", + submissionState = null, + newActivity = false + ) + return PlannerItemEntity(plannerItem, courseId) + } + + private fun createPlannerItems(ids: List, courseId: Long): List { + return ids.map { createPlannerItem(it, courseId, "Item $it") } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt index 4a2b9f310d..ee18b3124f 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt @@ -69,6 +69,7 @@ import com.instructure.pandautils.room.offline.daos.ModuleItemDao import com.instructure.pandautils.room.offline.daos.ModuleNameDao import com.instructure.pandautils.room.offline.daos.ModuleObjectDao import com.instructure.pandautils.room.offline.daos.PageDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.daos.PlannerOverrideDao import com.instructure.pandautils.room.offline.daos.QuizDao import com.instructure.pandautils.room.offline.daos.RemoteFileDao @@ -202,6 +203,11 @@ class OfflineModule { return appDatabase.groupDao() } + @Provides + fun providePlannerItemDao(appDatabase: OfflineDatabase): PlannerItemDao { + return appDatabase.plannerItemDao() + } + @Provides fun providePlannerOverrideDao(appDatabase: OfflineDatabase): PlannerOverrideDao { return appDatabase.plannerOverrideDao() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt index 8b9e24ac21..a0ee5c09d4 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt @@ -33,6 +33,7 @@ import com.instructure.canvasapi2.apis.GroupAPI import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.apis.PlannerAPI import com.instructure.canvasapi2.apis.QuizAPI import com.instructure.canvasapi2.apis.StudioApi import com.instructure.canvasapi2.apis.UserAPI @@ -53,6 +54,7 @@ import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao import com.instructure.pandautils.room.offline.daos.LocalFileDao import com.instructure.pandautils.room.offline.daos.PageDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.daos.QuizDao import com.instructure.pandautils.room.offline.daos.StudioMediaProgressDao import com.instructure.pandautils.room.offline.facade.AssignmentFacade @@ -148,7 +150,9 @@ class OfflineSyncModule { firebaseCrashlytics: FirebaseCrashlytics, fileSync: FileSync, customGradeStatusDao: CustomGradeStatusDao, - customGradeStatusesManager: CustomGradeStatusesManager + customGradeStatusesManager: CustomGradeStatusesManager, + plannerApi: PlannerAPI.PlannerInterface, + plannerItemDao: PlannerItemDao ): CourseSync { return CourseSync( courseApi, @@ -156,6 +160,7 @@ class OfflineSyncModule { userApi, assignmentApi, calendarEventApi, + plannerApi, courseSyncSettingsDao, pageFacade, userFacade, @@ -186,7 +191,8 @@ class OfflineSyncModule { firebaseCrashlytics, fileSync, customGradeStatusDao, - customGradeStatusesManager + customGradeStatusesManager, + plannerItemDao ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt index 256d0d0a07..1a5b9b51dc 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt @@ -31,6 +31,7 @@ import com.instructure.canvasapi2.apis.FileFolderAPI import com.instructure.canvasapi2.apis.GroupAPI import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.apis.PlannerAPI import com.instructure.canvasapi2.apis.QuizAPI import com.instructure.canvasapi2.apis.UserAPI import com.instructure.canvasapi2.builders.RestParams @@ -56,12 +57,14 @@ import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao import com.instructure.pandautils.room.offline.daos.CustomGradeStatusDao import com.instructure.pandautils.room.offline.daos.FileFolderDao import com.instructure.pandautils.room.offline.daos.PageDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.daos.QuizDao import com.instructure.pandautils.room.offline.entities.CourseFeaturesEntity import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity import com.instructure.pandautils.room.offline.entities.CustomGradeStatusEntity import com.instructure.pandautils.room.offline.entities.FileFolderEntity +import com.instructure.pandautils.room.offline.entities.PlannerItemEntity import com.instructure.pandautils.room.offline.entities.QuizEntity import com.instructure.pandautils.room.offline.facade.AssignmentFacade import com.instructure.pandautils.room.offline.facade.ConferenceFacade @@ -84,6 +87,7 @@ class CourseSync( private val userApi: UserAPI.UsersInterface, private val assignmentApi: AssignmentAPI.AssignmentInterface, private val calendarEventApi: CalendarEventAPI.CalendarEventInterface, + private val plannerApi: PlannerAPI.PlannerInterface, private val courseSyncSettingsDao: CourseSyncSettingsDao, private val pageFacade: PageFacade, private val userFacade: UserFacade, @@ -114,7 +118,8 @@ class CourseSync( private val firebaseCrashlytics: FirebaseCrashlytics, private val fileSync: FileSync, private val customGradeStatusDao: CustomGradeStatusDao, - private val customGradeStatusesManager: CustomGradeStatusesManager + private val customGradeStatusesManager: CustomGradeStatusesManager, + private val plannerItemDao: PlannerItemDao ) { private val additionalFileIdsToSync = mutableMapOf>() @@ -245,9 +250,28 @@ class CourseSync( scheduleItems.addAll(assignmentEvents) scheduleItemFacade.insertScheduleItems(scheduleItems, courseId) + + val plannerItems = fetchPlannerItems(courseId) + plannerItemDao.deleteAllByCourseId(courseId) + plannerItemDao.insertAll(plannerItems) } } + private suspend fun fetchPlannerItems(courseId: Long): List { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true, shouldLoginOnTokenError = false) + val plannerItems = plannerApi.getPlannerItems( + null, + null, + listOf("course_$courseId"), + "new_activity", + restParams + ).depaginate { + plannerApi.nextPagePlannerItems(it, restParams) + }.dataOrThrow + + return plannerItems.map { PlannerItemEntity(it, courseId) } + } + private suspend fun fetchCalendarEvents(courseId: Long): List { val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true, shouldLoginOnTokenError = false) val calendarEvents = calendarEventApi.getCalendarEvents( diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt index ff10789041..b500e3c633 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt @@ -68,6 +68,7 @@ import com.instructure.pandautils.room.offline.daos.ModuleItemDao import com.instructure.pandautils.room.offline.daos.ModuleNameDao import com.instructure.pandautils.room.offline.daos.ModuleObjectDao import com.instructure.pandautils.room.offline.daos.PageDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.daos.PlannerOverrideDao import com.instructure.pandautils.room.offline.daos.QuizDao import com.instructure.pandautils.room.offline.daos.RemoteFileDao @@ -139,6 +140,7 @@ import com.instructure.pandautils.room.offline.entities.ModuleNameEntity import com.instructure.pandautils.room.offline.entities.ModuleObjectEntity import com.instructure.pandautils.room.offline.entities.NeedsGradingCountEntity import com.instructure.pandautils.room.offline.entities.PageEntity +import com.instructure.pandautils.room.offline.entities.PlannerItemEntity import com.instructure.pandautils.room.offline.entities.PlannerOverrideEntity import com.instructure.pandautils.room.offline.entities.QuizEntity import com.instructure.pandautils.room.offline.entities.RemoteFileEntity @@ -197,6 +199,7 @@ import com.instructure.pandautils.room.offline.entities.UserEntity ModuleObjectEntity::class, NeedsGradingCountEntity::class, PageEntity::class, + PlannerItemEntity::class, PlannerOverrideEntity::class, RemoteFileEntity::class, RubricCriterionAssessmentEntity::class, @@ -233,7 +236,7 @@ import com.instructure.pandautils.room.offline.entities.UserEntity CustomGradeStatusEntity::class, CheckpointEntity::class, SubAssignmentSubmissionEntity::class - ], version = 6 + ], version = 7 ) @TypeConverters(value = [Converters::class, OfflineConverters::class]) abstract class OfflineDatabase : RoomDatabase() { @@ -272,6 +275,8 @@ abstract class OfflineDatabase : RoomDatabase() { abstract fun groupDao(): GroupDao + abstract fun plannerItemDao(): PlannerItemDao + abstract fun plannerOverrideDao(): PlannerOverrideDao abstract fun discussionTopicHeaderDao(): DiscussionTopicHeaderDao diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt index 5b09de227b..51ea4c1135 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt @@ -155,5 +155,35 @@ val offlineDatabaseMigrations = arrayOf( "`isGradeMatchesCurrentSubmission` INTEGER NOT NULL," + "FOREIGN KEY(`submissionId`, `submissionAttempt`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE)" ) + }, + createMigration(6, 7) { database -> + database.execSQL( + "CREATE TABLE IF NOT EXISTS `PlannerItemEntity` (" + + "`id` INTEGER PRIMARY KEY NOT NULL," + + "`courseId` INTEGER," + + "`groupId` INTEGER," + + "`userId` INTEGER," + + "`contextType` TEXT," + + "`contextName` TEXT," + + "`plannableType` TEXT NOT NULL," + + "`plannableId` INTEGER NOT NULL," + + "`plannableTitle` TEXT," + + "`plannableDetails` TEXT," + + "`plannableTodoDate` TEXT," + + "`plannableEndAt` INTEGER," + + "`plannableAllDay` INTEGER," + + "`plannableCourseId` INTEGER," + + "`plannableGroupId` INTEGER," + + "`plannableUserId` INTEGER," + + "`plannableDate` INTEGER NOT NULL," + + "`htmlUrl` TEXT," + + "`submissionStateSubmitted` INTEGER," + + "`submissionStateExcused` INTEGER," + + "`submissionStateGraded` INTEGER," + + "`newActivity` INTEGER," + + "`plannerOverrideId` INTEGER," + + "`plannerOverrideMarkedComplete` INTEGER," + + "FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON DELETE CASCADE)" + ) } ) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PlannerItemDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PlannerItemDao.kt new file mode 100644 index 0000000000..a479c982d3 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PlannerItemDao.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025 - 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.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.PlannerItemEntity + +@Dao +interface PlannerItemDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: PlannerItemEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Delete + suspend fun delete(entity: PlannerItemEntity) + + @Query("DELETE FROM PlannerItemEntity WHERE courseId=:courseId") + suspend fun deleteAllByCourseId(courseId: Long) + + @Update + suspend fun update(entity: PlannerItemEntity) + + @Query("SELECT * FROM PlannerItemEntity WHERE id=:id") + suspend fun findById(id: Long): PlannerItemEntity? + + @Query("SELECT * FROM PlannerItemEntity WHERE courseId IN (:courseIds)") + suspend fun findByCourseIds(courseIds: List): List + + @Query("SELECT * FROM PlannerItemEntity WHERE courseId=:courseId") + suspend fun findByCourseId(courseId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/PlannerItemEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/PlannerItemEntity.kt new file mode 100644 index 0000000000..3c976f0a2a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/PlannerItemEntity.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2025 - 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.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.* +import java.util.* + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class PlannerItemEntity( + @PrimaryKey + val id: Long, + val courseId: Long?, + val groupId: Long?, + val userId: Long?, + val contextType: String?, + val contextName: String?, + val plannableType: String, + val plannableId: Long, + val plannableTitle: String?, + val plannableDetails: String?, + val plannableTodoDate: String?, + val plannableEndAt: Long?, + val plannableAllDay: Boolean?, + val plannableCourseId: Long?, + val plannableGroupId: Long?, + val plannableUserId: Long?, + val plannableDate: Long, + val htmlUrl: String?, + val submissionStateSubmitted: Boolean?, + val submissionStateExcused: Boolean?, + val submissionStateGraded: Boolean?, + val newActivity: Boolean?, + val plannerOverrideId: Long?, + val plannerOverrideMarkedComplete: Boolean? +) { + constructor(plannerItem: PlannerItem, courseId: Long?) : this( + id = plannerItem.plannable.id, + courseId = courseId, + groupId = plannerItem.groupId, + userId = plannerItem.userId, + contextType = plannerItem.contextType, + contextName = plannerItem.contextName, + plannableType = plannerItem.plannableType.name, + plannableId = plannerItem.plannable.id, + plannableTitle = plannerItem.plannable.title, + plannableDetails = plannerItem.plannable.details, + plannableTodoDate = plannerItem.plannable.todoDate, + plannableEndAt = plannerItem.plannable.endAt?.time, + plannableAllDay = plannerItem.plannable.allDay, + plannableCourseId = plannerItem.plannable.courseId, + plannableGroupId = plannerItem.plannable.groupId, + plannableUserId = plannerItem.plannable.userId, + plannableDate = plannerItem.plannableDate.time, + htmlUrl = plannerItem.htmlUrl, + submissionStateSubmitted = plannerItem.submissionState?.submitted, + submissionStateExcused = plannerItem.submissionState?.excused, + submissionStateGraded = plannerItem.submissionState?.graded, + newActivity = plannerItem.newActivity, + plannerOverrideId = plannerItem.plannerOverride?.id, + plannerOverrideMarkedComplete = plannerItem.plannerOverride?.markedComplete + ) + + fun toApiModel(): PlannerItem { + val plannable = Plannable( + id = plannableId, + title = plannableTitle ?: "", + courseId = plannableCourseId, + groupId = plannableGroupId, + userId = plannableUserId, + pointsPossible = null, + dueAt = null, + assignmentId = null, + todoDate = plannableTodoDate, + startAt = null, + endAt = plannableEndAt?.let { Date(it) }, + details = plannableDetails, + allDay = plannableAllDay + ) + + val submissionState = if (submissionStateSubmitted != null || submissionStateExcused != null || submissionStateGraded != null) { + SubmissionState( + submitted = submissionStateSubmitted ?: false, + excused = submissionStateExcused ?: false, + graded = submissionStateGraded ?: false + ) + } else null + + val plannerOverride = if (plannerOverrideId != null) { + PlannerOverride( + id = plannerOverrideId, + plannableType = PlannableType.valueOf(plannableType), + plannableId = plannableId, + markedComplete = plannerOverrideMarkedComplete ?: false + ) + } else null + + return PlannerItem( + courseId = courseId, + groupId = groupId, + userId = userId, + contextType = contextType, + contextName = contextName, + plannableType = PlannableType.valueOf(plannableType), + plannable = plannable, + plannableDate = Date(plannableDate), + htmlUrl = htmlUrl, + submissionState = submissionState, + newActivity = newActivity, + plannerOverride = plannerOverride + ) + } +} \ No newline at end of file From 218954504cf2860741922fba2dd9aca966ba8fea Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Tue, 14 Oct 2025 23:34:09 +0200 Subject: [PATCH 42/94] Fixed a bug where on the Syllabus Summary page the quizzes icons was not shown correctly. (All classic quizzes had locked icon, and all new quizzes had the default assignment icon). (#3301) refs: MBL-18749 affects: Student, Teacher, Parent release note: Fix a bug where Syllabus Summary pages does not show correct icons for classic (locked) and new quizzes (default assignment icon). --- .../student/mobius/syllabus/SyllabusPresenter.kt | 2 ++ .../features/syllabus/SyllabusPresenter.kt | 2 ++ .../pandautils/utils/ScheduleItemExtensions.kt | 16 ++++++++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusPresenter.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusPresenter.kt index 99c20cf81c..a9a54e15d1 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusPresenter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusPresenter.kt @@ -93,6 +93,8 @@ object SyllabusPresenter : Presenter { private fun getAssignmentIcon(assignment: Assignment) = when { assignment.getSubmissionTypes().contains(Assignment.SubmissionType.ONLINE_QUIZ) -> com.instructure.student.R.drawable.ic_quiz + assignment.getSubmissionTypes().contains(Assignment.SubmissionType.EXTERNAL_TOOL) && + assignment.externalToolAttributes?.url?.contains("quiz-lti") == true -> com.instructure.student.R.drawable.ic_quiz assignment.getSubmissionTypes().contains(Assignment.SubmissionType.DISCUSSION_TOPIC) -> com.instructure.student.R.drawable.ic_discussion else -> com.instructure.student.R.drawable.ic_assignment } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusPresenter.kt index 4dedba6a66..fe830a39b5 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusPresenter.kt @@ -93,6 +93,8 @@ class SyllabusPresenter : Presenter { private fun getAssignmentIcon(assignment: Assignment): Int { return when { assignment.getSubmissionTypes().contains(Assignment.SubmissionType.ONLINE_QUIZ) -> R.drawable.ic_quiz + assignment.getSubmissionTypes().contains(Assignment.SubmissionType.EXTERNAL_TOOL) && + assignment.externalToolAttributes?.url?.contains("quiz-lti") == true -> R.drawable.ic_quiz assignment.getSubmissionTypes().contains(Assignment.SubmissionType.DISCUSSION_TOPIC) -> R.drawable.ic_discussion else -> R.drawable.ic_assignment } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ScheduleItemExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ScheduleItemExtensions.kt index fc3b1ff5bb..1ca306584a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ScheduleItemExtensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ScheduleItemExtensions.kt @@ -26,10 +26,16 @@ import java.util.Date val ScheduleItem.iconRes: Int get() { + val isClassicQuiz = this.assignment?.getSubmissionTypes()?.contains(SubmissionType.ONLINE_QUIZ).orDefault() + val isNewQuiz = this.assignment?.getSubmissionTypes()?.contains(SubmissionType.EXTERNAL_TOOL).orDefault() && + this.assignment?.externalToolAttributes?.url?.contains("quiz-lti").orDefault() + return when { this.type == "event" -> R.drawable.ic_calendar + isClassicQuiz && this.assignment?.isLocked.orDefault() -> R.drawable.ic_lock // For classic quizzes, use isLocked instead of lockedForUser (classic quizzes always return lockedForUser=true from API) + isClassicQuiz -> R.drawable.ic_quiz this.assignment?.lockedForUser.orDefault() -> R.drawable.ic_lock - this.assignment?.getSubmissionTypes()?.contains(SubmissionType.ONLINE_QUIZ).orDefault() -> R.drawable.ic_quiz + isNewQuiz -> R.drawable.ic_quiz this.assignment?.getSubmissionTypes()?.contains(SubmissionType.DISCUSSION_TOPIC).orDefault() -> R.drawable.ic_discussion else -> R.drawable.ic_assignment } @@ -37,10 +43,16 @@ val ScheduleItem.iconRes: Int val ScheduleItem.contentDescriptionRes: Int get() { + val isClassicQuiz = this.assignment?.getSubmissionTypes()?.contains(SubmissionType.ONLINE_QUIZ).orDefault() + val isNewQuiz = this.assignment?.getSubmissionTypes()?.contains(SubmissionType.EXTERNAL_TOOL).orDefault() && + this.assignment?.externalToolAttributes?.url?.contains("quiz-lti").orDefault() + return when { this.type == "event" -> R.string.a11y_summaryEventContentDescription + isClassicQuiz && this.assignment?.isLocked.orDefault() -> R.string.a11y_summaryLockedContentDescription // For classic quizzes, use isLocked instead of lockedForUser (classic quizzes always return lockedForUser=true from API) + isClassicQuiz -> R.string.a11y_summaryQuizContentDescription this.assignment?.lockedForUser.orDefault() -> R.string.a11y_summaryLockedContentDescription - this.assignment?.getSubmissionTypes()?.contains(SubmissionType.ONLINE_QUIZ).orDefault() -> R.string.a11y_summaryQuizContentDescription + isNewQuiz -> R.string.a11y_summaryQuizContentDescription this.assignment?.getSubmissionTypes()?.contains(SubmissionType.DISCUSSION_TOPIC).orDefault() -> R.string.a11y_summaryDiscussionContentDescription else -> R.string.a11y_summaryAssignmentContentDescription } From 95f2d8f6d9fab957a84e6891f70bce9304dd4eb9 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:18:48 +0200 Subject: [PATCH 43/94] [MBL-19124][Student] Discussion list DCP support refs: MBL-19124 affects: Student release note: Added support for displaying checkpoint dates and assignment points in discussion list with offline capability. --- .../interaction/DiscussionsInteractionTest.kt | 89 +++++++++- .../ui/pages/classic/DiscussionListPage.kt | 18 ++ .../list/adapter/DiscussionListHolder.kt | 38 ++++- .../main/res/layout/viewholder_discussion.xml | 13 +- .../canvasapi2/apis/DiscussionAPI.kt | 2 +- .../pandautils/di/OfflineModule.kt | 8 +- .../facade/DiscussionTopicHeaderFacade.kt | 22 ++- .../facade/DiscussionTopicHeaderFacadeTest.kt | 156 +++++++++++++++++- 8 files changed, 332 insertions(+), 14 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt index 8c498c7184..ecdc8a41d4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt @@ -33,16 +33,19 @@ import com.instructure.canvas.espresso.mockcanvas.addReplyToDiscussion import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.DiscussionEntry import com.instructure.canvasapi2.models.RemoteFile import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.toApiString import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Assert.assertNotNull import org.junit.Test +import java.util.Calendar // Note: Tests course discussions, not group discussions. @HiltAndroidTest @@ -721,6 +724,90 @@ class DiscussionsInteractionTest : StudentTest() { return data } + // Tests that checkpoint due dates are displayed in the discussion list + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + fun testDiscussionList_displayCheckpointDates() { + val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) + val course = data.courses.values.first() + val teacher = data.users.values.first() + + val calendar1 = Calendar.getInstance().apply { set(2023, 0, 29, 13, 30, 0) } + val expectedDate1 = "Due Jan 29, 2023 1:30 PM" + val checkpoint1DueDate = calendar1.time.toApiString() + + val calendar2 = Calendar.getInstance().apply { set(2023, 0, 31, 15, 30, 0) } + val expectedDate2 = "Due Jan 31, 2023 3:30 PM" + val checkpoint2DueDate = calendar2.time.toApiString() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = checkpoint1DueDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = checkpoint2DueDate, + pointsPossible = 10.0 + ) + ) + + val assignment = data.addAssignment( + courseId = course.id, + submissionTypeList = listOf(Assignment.SubmissionType.DISCUSSION_TOPIC), + name = "Discussion with Checkpoints", + pointsPossible = 20, + checkpoints = checkpoints + ) + + data.addDiscussionTopicToCourse( + course = course, + user = teacher, + topicTitle = "Discussion with Checkpoints", + topicDescription = "Test checkpoints display", + assignment = assignment + ) + + courseBrowserPage.selectDiscussions() + discussionListPage.pullToUpdate() + discussionListPage.assertTopicDisplayed("Discussion with Checkpoints") + discussionListPage.assertCheckpointDueDates("Discussion with Checkpoints", "$expectedDate1\n$expectedDate2") + } + + // Tests that points possible are displayed in the discussion list + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + fun testDiscussionList_displayPointsPossible() { + val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) + val course = data.courses.values.first() + val teacher = data.users.values.first() + + val assignment = data.addAssignment( + courseId = course.id, + submissionTypeList = listOf(Assignment.SubmissionType.DISCUSSION_TOPIC), + name = "Discussion with Points", + pointsPossible = 15 + ) + + data.addDiscussionTopicToCourse( + course = course, + user = teacher, + topicTitle = "Discussion with Points", + topicDescription = "Test points display", + assignment = assignment + ) + + courseBrowserPage.selectDiscussions() + discussionListPage.pullToUpdate() + discussionListPage.assertTopicDisplayed("Discussion with Points") + + // Verify points are displayed in readUnreadCounts (alongside reply counts) + discussionListPage.assertPointsDisplayed("Discussion with Points", 15) + } + companion object { // Creates an HTML attachment/file which can then be attached to a topic header or reply. fun createHtmlAttachment(data: MockCanvas, html: String): RemoteFile { @@ -743,7 +830,5 @@ class DiscussionsInteractionTest : StudentTest() { return attachment } - } - } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt index 7c9e3433e8..159d39ea88 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt @@ -182,4 +182,22 @@ open class DiscussionListPage(val searchable: Searchable) : BasePage(R.id.discus val matcher = allOf(withId(R.id.dueDate), withText(containsString(expectedDateString)), hasSibling(allOf(withId(R.id.discussionTitle), withText(topicTitle)))) onView(matcher).scrollTo().assertDisplayed() } + + fun assertCheckpointDueDates(topicTitle: String, expectedDateString: String) { + val matcher = allOf( + withId(R.id.checkpointDueDates), + withText(containsString(expectedDateString)), + hasSibling(allOf(withId(R.id.discussionTitle), withText(topicTitle))) + ) + onView(matcher).scrollTo().assertDisplayed() + } + + fun assertPointsDisplayed(topicTitle: String, points: Int) { + val matcher = allOf( + withId(R.id.readUnreadCounts), + withText(containsString("$points pts")), + hasSibling(allOf(withId(R.id.discussionTitle), withText(topicTitle))) + ) + onView(matcher).scrollTo().assertDisplayed() + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListHolder.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListHolder.kt index e9673e4bdf..60616980af 100644 --- a/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListHolder.kt @@ -20,13 +20,16 @@ import android.content.Context import android.view.View import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.localized import com.instructure.pandautils.utils.onClick +import com.instructure.pandautils.utils.orderedCheckpoints import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setInvisible import com.instructure.pandautils.utils.setVisible +import com.instructure.pandautils.utils.toFormattedString import com.instructure.student.R import com.instructure.student.databinding.ViewholderDiscussionBinding import java.util.Date @@ -82,6 +85,9 @@ class DiscussionListHolder(view: View) : RecyclerView.ViewHolder(view) { discussionIcon.hideNestedIcon() } + checkpointDueDates.text = getFormattedCheckpointDates(context, discussionTopicHeader.assignment) + checkpointDueDates.setVisible(checkpointDueDates.text.isNotEmpty()) + dueDate.text = when { isAssignmentType -> { if (discussionTopicHeader.assignment!!.dueDate == null) getFormattedLastPost( @@ -106,17 +112,26 @@ class DiscussionListHolder(view: View) : RecyclerView.ViewHolder(view) { statusIndicator.setInvisible() } - val entryCountString = - context.resources.getQuantityString(R.plurals.utils_discussionsReplies, entryCount, entryCount.localized) + val entryCountString = context.resources.getQuantityString( + R.plurals.utils_discussionsReplies, + entryCount, + entryCount.localized + ) val unreadCountString = context.resources.getQuantityString( R.plurals.utils_discussionsUnread, discussionTopicHeader.unreadCount, unreadDisplayCount ) + val pointsText = discussionTopicHeader.assignment?.pointsPossible?.toInt()?.let { + context.resources.getQuantityString( + R.plurals.quantityPointsAbbreviated, + it, + it + ) + } - readUnreadCounts.text = context.getString( - R.string.utils_discussionsUnreadRepliesBlank, - entryCountString, context.getString(R.string.utils_dotWithSpaces), unreadCountString + readUnreadCounts.text = listOfNotNull(pointsText, entryCountString, unreadCountString).joinToString( + context.getString(R.string.utils_dotWithSpaces) ) } @@ -137,6 +152,19 @@ class DiscussionListHolder(view: View) : RecyclerView.ViewHolder(view) { return context.getString(R.string.utils_dueDateAtTime).format(dueDate, dueTime) } + private fun getFormattedCheckpointDates(context: Context, assignment: Assignment?): String { + return assignment?.orderedCheckpoints.orEmpty().joinToString("\n") { checkpoint -> + getDueDateText(context, checkpoint.dueDate) + } + } + + private fun getDueDateText(context: Context, dueDate: Date?): String { + return when { + dueDate == null -> context.getString(R.string.noDueDate) + else -> context.getString(R.string.due, dueDate.toFormattedString()) + } + } + companion object { const val HOLDER_RES_ID = R.layout.viewholder_discussion } diff --git a/apps/student/src/main/res/layout/viewholder_discussion.xml b/apps/student/src/main/res/layout/viewholder_discussion.xml index ccccfd5942..366e27a3e9 100644 --- a/apps/student/src/main/res/layout/viewholder_discussion.xml +++ b/apps/student/src/main/res/layout/viewholder_discussion.xml @@ -63,6 +63,17 @@ android:layout_toStartOf="@+id/discussionOverflow" tools:text="Beginning of the Biological Existence of Mankind in the Jungles of South Asia Hodor Hodor" /> + + diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt index e4e81334f3..eef2d1c8a9 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt @@ -79,7 +79,7 @@ object DiscussionAPI { @GET("{contextType}/{contextId}/discussion_topics?override_assignment_dates=true&include[]=all_dates&include[]=overrides&include[]=sections") fun getFirstPageDiscussionTopicHeaders(@Path("contextType") contextType: String, @Path("contextId") contextId: Long): Call> - @GET("{contextType}/{contextId}/discussion_topics?override_assignment_dates=true&include[]=all_dates&include[]=overrides&include[]=sections") + @GET("{contextType}/{contextId}/discussion_topics?override_assignment_dates=true&include[]=all_dates&include[]=overrides&include[]=sections&include[]=checkpoints") suspend fun getFirstPageDiscussionTopicHeaders(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Tag params: RestParams): DataResult> @GET("{contextType}/{contextId}/discussion_topics/{topicId}?include[]=sections") diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt index ee18b3124f..9fbb261062 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt @@ -31,7 +31,6 @@ import com.instructure.pandautils.room.offline.daos.AssignmentSetDao import com.instructure.pandautils.room.offline.daos.AttachmentDao import com.instructure.pandautils.room.offline.daos.AuthorDao import com.instructure.pandautils.room.offline.daos.CheckpointDao -import com.instructure.pandautils.room.offline.daos.SubAssignmentSubmissionDao import com.instructure.pandautils.room.offline.daos.ConferenceDao import com.instructure.pandautils.room.offline.daos.ConferenceRecodingDao import com.instructure.pandautils.room.offline.daos.CourseDao @@ -81,6 +80,7 @@ import com.instructure.pandautils.room.offline.daos.ScheduleItemAssignmentOverri import com.instructure.pandautils.room.offline.daos.ScheduleItemDao import com.instructure.pandautils.room.offline.daos.SectionDao import com.instructure.pandautils.room.offline.daos.StudioMediaProgressDao +import com.instructure.pandautils.room.offline.daos.SubAssignmentSubmissionDao import com.instructure.pandautils.room.offline.daos.SubmissionCommentDao import com.instructure.pandautils.room.offline.daos.SubmissionDao import com.instructure.pandautils.room.offline.daos.SyncSettingsDao @@ -374,6 +374,8 @@ class OfflineModule { localFileDao: LocalFileDao, discussionTopicRemoteFileDao: DiscussionTopicRemoteFileDao, offlineDatabase: OfflineDatabase, + assignmentDao: AssignmentDao, + checkpointDao: CheckpointDao ): DiscussionTopicHeaderFacade { return DiscussionTopicHeaderFacade( discussionTopicHeaderDao, @@ -382,7 +384,9 @@ class OfflineModule { remoteFileDao, localFileDao, discussionTopicRemoteFileDao, - offlineDatabase + offlineDatabase, + assignmentDao, + checkpointDao ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicHeaderFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicHeaderFacade.kt index d2c6fcb53a..46ce56560e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicHeaderFacade.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicHeaderFacade.kt @@ -20,6 +20,8 @@ package com.instructure.pandautils.room.offline.facade import androidx.room.withTransaction import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.daos.AssignmentDao +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.DiscussionParticipantDao import com.instructure.pandautils.room.offline.daos.DiscussionTopicHeaderDao import com.instructure.pandautils.room.offline.daos.DiscussionTopicPermissionDao @@ -39,7 +41,9 @@ class DiscussionTopicHeaderFacade( private val remoteFileDao: RemoteFileDao, private val localFileDao: LocalFileDao, private val discussionTopicRemoteFileDao: DiscussionTopicRemoteFileDao, - private val offlineDatabase: OfflineDatabase + private val offlineDatabase: OfflineDatabase, + private val assignmentDao: AssignmentDao, + private val checkpointDao: CheckpointDao ) { suspend fun insertDiscussion(discussionTopicHeader: DiscussionTopicHeader, courseId: Long): Long { discussionTopicHeader.author?.let { discussionParticipantDao.insert(DiscussionParticipantEntity(it)) } @@ -127,6 +131,20 @@ class DiscussionTopicHeaderFacade( it.copy(url = path) } .map { it.toApiModel() } - return discussionTopicHeaderEntity.toApiModel(authorEntity?.toApiModel(), permissions = permission?.toApiModel(), attachments = attachments) + val assignment = discussionTopicHeaderEntity.assignmentId?.let { + assignmentDao.findById(it)?.let { assignmentEntity -> + val checkpoints = checkpointDao.findByAssignmentId(assignmentEntity.id).map { checkpoint -> + checkpoint.toApiModel() + } + assignmentEntity.toApiModel(checkpoints = checkpoints) + } + } + + return discussionTopicHeaderEntity.toApiModel( + authorEntity?.toApiModel(), + assignment, + permission?.toApiModel(), + attachments + ) } } \ No newline at end of file diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicHeaderFacadeTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicHeaderFacadeTest.kt index b0a6fd9078..98710aabeb 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicHeaderFacadeTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicHeaderFacadeTest.kt @@ -18,23 +18,30 @@ package com.instructure.pandautils.room.offline.facade import androidx.room.withTransaction +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.DiscussionParticipant import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.DiscussionTopicPermission import com.instructure.canvasapi2.models.RemoteFile import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.daos.AssignmentDao +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.DiscussionParticipantDao import com.instructure.pandautils.room.offline.daos.DiscussionTopicHeaderDao import com.instructure.pandautils.room.offline.daos.DiscussionTopicPermissionDao import com.instructure.pandautils.room.offline.daos.DiscussionTopicRemoteFileDao import com.instructure.pandautils.room.offline.daos.LocalFileDao import com.instructure.pandautils.room.offline.daos.RemoteFileDao +import com.instructure.pandautils.room.offline.entities.AssignmentEntity +import com.instructure.pandautils.room.offline.entities.CheckpointEntity import com.instructure.pandautils.room.offline.entities.DiscussionParticipantEntity import com.instructure.pandautils.room.offline.entities.DiscussionTopicHeaderEntity import com.instructure.pandautils.room.offline.entities.DiscussionTopicPermissionEntity import com.instructure.pandautils.room.offline.entities.DiscussionTopicRemoteFileEntity import com.instructure.pandautils.room.offline.entities.LocalFileEntity import com.instructure.pandautils.room.offline.entities.RemoteFileEntity +import com.instructure.pandautils.utils.orDefault import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -58,8 +65,20 @@ class DiscussionTopicHeaderFacadeTest { private val localFileDao: LocalFileDao = mockk(relaxed = true) private val discussionTopicRemoteFileDao: DiscussionTopicRemoteFileDao = mockk(relaxed = true) private val offlineDatabase: OfflineDatabase = mockk(relaxed = true) + private val assignmentDao: AssignmentDao = mockk(relaxed = true) + private val checkpointDao: CheckpointDao = mockk(relaxed = true) - private val facade = DiscussionTopicHeaderFacade(discussionTopicHeaderDao, discussionParticipantDao, discussionTopicPermissionDao, remoteFileDao, localFileDao, discussionTopicRemoteFileDao, offlineDatabase) + private val facade = DiscussionTopicHeaderFacade( + discussionTopicHeaderDao, + discussionParticipantDao, + discussionTopicPermissionDao, + remoteFileDao, + localFileDao, + discussionTopicRemoteFileDao, + offlineDatabase, + assignmentDao, + checkpointDao + ) @Before fun setup() { @@ -73,6 +92,8 @@ class DiscussionTopicHeaderFacadeTest { coEvery { offlineDatabase.withTransaction(capture(transactionLambda)) } coAnswers { transactionLambda.captured.invoke() } + + coEvery { assignmentDao.findById(0) } returns null } @After @@ -185,4 +206,137 @@ class DiscussionTopicHeaderFacadeTest { Assert.assertEquals(discussionTopicHeader, result[0]) Assert.assertEquals(discussionTopicHeader2, result[1]) } + + @Test + fun `getDiscussionTopicHeaderById should return discussion with assignment when assignmentId is set`() = runTest { + val discussionTopicHeaderId = 1L + val assignmentId = 10L + val discussionParticipant = DiscussionParticipant(id = 1L, displayName = "displayName") + val discussionPermission = DiscussionTopicPermission() + val assignment = Assignment(id = assignmentId, name = "Test Assignment", pointsPossible = 15.0) + val discussionTopicHeader = DiscussionTopicHeader( + id = discussionTopicHeaderId, + author = discussionParticipant, + permissions = discussionPermission, + title = "Title", + assignmentId = assignmentId, + assignment = assignment + ) + + coEvery { discussionParticipantDao.findById(1L) } returns DiscussionParticipantEntity(discussionParticipant) + coEvery { discussionTopicHeaderDao.findById(discussionTopicHeaderId) } returns DiscussionTopicHeaderEntity( + discussionTopicHeader, + 1 + ) + coEvery { discussionTopicPermissionDao.findByDiscussionTopicHeaderId(discussionTopicHeaderId) } returns DiscussionTopicPermissionEntity( + discussionPermission, + discussionTopicHeaderId + ) + coEvery { discussionTopicRemoteFileDao.findByDiscussionId(discussionTopicHeaderId) } returns emptyList() + coEvery { assignmentDao.findById(assignmentId) } returns AssignmentEntity(assignment, null, null, 1L, null) + coEvery { checkpointDao.findByAssignmentId(assignmentId) } returns emptyList() + + val result = facade.getDiscussionTopicHeaderById(discussionTopicHeaderId)!! + + Assert.assertEquals(discussionParticipant, result.author) + Assert.assertNotNull(result.assignment) + Assert.assertEquals(assignmentId, result.assignment?.id) + Assert.assertEquals(15.0, result.assignment?.pointsPossible.orDefault(), 0.01) + + coVerify { assignmentDao.findById(assignmentId) } + coVerify { checkpointDao.findByAssignmentId(assignmentId) } + } + + @Test + fun `getDiscussionTopicHeaderById should return discussion with assignment and checkpoints`() = runTest { + val discussionTopicHeaderId = 1L + val assignmentId = 10L + val discussionParticipant = DiscussionParticipant(id = 1L, displayName = "displayName") + val discussionPermission = DiscussionTopicPermission() + val checkpoint1 = Checkpoint( + tag = "reply_to_topic", + name = "Reply to Topic", + pointsPossible = 5.0, + dueAt = "2023-01-29T23:59:00Z" + ) + val checkpoint2 = Checkpoint( + tag = "reply_to_entry", + name = "Reply to Entry", + pointsPossible = 5.0, + dueAt = "2023-01-31T23:59:00Z" + ) + val assignment = Assignment( + id = assignmentId, + name = "Test Assignment", + pointsPossible = 10.0, + checkpoints = listOf(checkpoint1, checkpoint2) + ) + val discussionTopicHeader = DiscussionTopicHeader( + id = discussionTopicHeaderId, + author = discussionParticipant, + permissions = discussionPermission, + title = "Title", + assignmentId = assignmentId, + assignment = assignment + ) + + coEvery { discussionParticipantDao.findById(1L) } returns DiscussionParticipantEntity(discussionParticipant) + coEvery { discussionTopicHeaderDao.findById(discussionTopicHeaderId) } returns DiscussionTopicHeaderEntity( + discussionTopicHeader, + 1 + ) + coEvery { discussionTopicPermissionDao.findByDiscussionTopicHeaderId(discussionTopicHeaderId) } returns DiscussionTopicPermissionEntity( + discussionPermission, + discussionTopicHeaderId + ) + coEvery { discussionTopicRemoteFileDao.findByDiscussionId(discussionTopicHeaderId) } returns emptyList() + coEvery { assignmentDao.findById(assignmentId) } returns AssignmentEntity(assignment, null, null, 1L, null) + coEvery { checkpointDao.findByAssignmentId(assignmentId) } returns listOf( + CheckpointEntity(checkpoint1, assignmentId), + CheckpointEntity(checkpoint2, assignmentId) + ) + + val result = facade.getDiscussionTopicHeaderById(discussionTopicHeaderId)!! + + Assert.assertEquals(discussionParticipant, result.author) + Assert.assertNotNull(result.assignment) + Assert.assertEquals(assignmentId, result.assignment?.id) + Assert.assertEquals(10.0, result.assignment?.pointsPossible.orDefault(), 0.01) + Assert.assertEquals(2, result.assignment?.checkpoints?.size) + Assert.assertEquals("Reply to Topic", result.assignment?.checkpoints?.get(0)?.name) + Assert.assertEquals("Reply to Entry", result.assignment?.checkpoints?.get(1)?.name) + + coVerify { assignmentDao.findById(assignmentId) } + coVerify { checkpointDao.findByAssignmentId(assignmentId) } + } + + @Test + fun `getDiscussionTopicHeaderById should return discussion without assignment when assignmentId is 0`() = runTest { + val discussionTopicHeaderId = 1L + val discussionParticipant = DiscussionParticipant(id = 1L, displayName = "displayName") + val discussionPermission = DiscussionTopicPermission() + val discussionTopicHeader = DiscussionTopicHeader( + id = discussionTopicHeaderId, + author = discussionParticipant, + permissions = discussionPermission, + title = "Title", + assignmentId = 0 + ) + + coEvery { discussionParticipantDao.findById(1L) } returns DiscussionParticipantEntity(discussionParticipant) + coEvery { discussionTopicHeaderDao.findById(discussionTopicHeaderId) } returns DiscussionTopicHeaderEntity( + discussionTopicHeader, + 1 + ) + coEvery { discussionTopicPermissionDao.findByDiscussionTopicHeaderId(discussionTopicHeaderId) } returns DiscussionTopicPermissionEntity( + discussionPermission, + discussionTopicHeaderId + ) + coEvery { discussionTopicRemoteFileDao.findByDiscussionId(discussionTopicHeaderId) } returns emptyList() + + val result = facade.getDiscussionTopicHeaderById(discussionTopicHeaderId)!! + + Assert.assertEquals(discussionParticipant, result.author) + Assert.assertNull(result.assignment) + } } \ No newline at end of file From 112cef7ac643832e2c8eeeb60f6aa4668e599210 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:20:04 +0200 Subject: [PATCH 44/94] [MBL-19020][Student] Navigating back to an unmarked "Mark as Done" requirement results in visual "Done" marking without unlocking subsequent items refs: MBL-19020 affects: Student release note: Fixed module progression issue where "Mark as Done" items were incorrectly marked as complete when navigating between module items, preventing proper sequential progression. --- .../modules/progression/CourseModuleProgressionFragment.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt index 43404506f2..8ceddd27c8 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt @@ -344,7 +344,11 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { repository.markAsRead(canvasContext, moduleItem) // Update the module item locally, needed to unlock modules as the user ViewPages through them - getCurrentModuleItem(currentPos)?.completionRequirement?.completed = true + // Only mark as completed if the requirement is satisfied by viewing (not must_mark_done) + val completionRequirement = getCurrentModuleItem(currentPos)?.completionRequirement + if (completionRequirement?.type != ModuleItem.MUST_MARK_DONE) { + completionRequirement?.completed = true + } setupNextModule(getModuleItemGroup(currentPos)) From 47fb6eac6b8bae489fd528d6c6487cabcc8c2ae5 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:40:13 +0200 Subject: [PATCH 45/94] [MBL-19330][All] Access denied error when trying to download files on android (#3306) refs: MBL-19330 affects: Student, Parent, Teacher release note: Fixed a bug where some embedded files couldn't be downloaded from pages. * Download with file downloader. * Fixed notifications. --- .../pages/details/PageDetailsFragment.kt | 8 + .../fragment/InternalWebviewFragment.kt | 6 + libs/pandares/src/main/res/values/strings.xml | 4 + libs/pandautils/src/main/AndroidManifest.xml | 1 + .../pandautils/di/FileDownloaderModule.kt | 17 +- .../utils/DownloadNotificationHelper.kt | 190 ++++++++++++++++++ .../pandautils/utils/FileDownloader.kt | 9 +- .../pandautils/views/CanvasWebView.kt | 18 +- 8 files changed, 247 insertions(+), 6 deletions(-) create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/utils/DownloadNotificationHelper.kt diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt index 3f1f3aebe7..9214a76371 100644 --- a/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt @@ -45,6 +45,7 @@ import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.navigation.WebViewRouter import com.instructure.pandautils.utils.BooleanArg +import com.instructure.pandautils.utils.FileDownloader import com.instructure.pandautils.utils.NullableStringArg import com.instructure.pandautils.utils.ParcelableArg import com.instructure.pandautils.utils.ViewStyler @@ -79,6 +80,9 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { @Inject lateinit var webViewRouter: WebViewRouter + @Inject + lateinit var fileDownloader: FileDownloader + private var loadHtmlJob: Job? = null private var pageName: String? by NullableStringArg(key = PAGE_NAME) private var page: Page by ParcelableArg(default = Page(), key = PAGE) @@ -351,6 +355,10 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { override fun handleBackPressed() = false + override fun downloadInternalMedia(mime: String?, url: String?, filename: String?) { + fileDownloader.downloadFileToDevice(url, filename, mime) + } + companion object { const val PAGE_NAME = "pageDetailsName" const val PAGE = "pageDetails" diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt index f09722105b..e64767651d 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt @@ -224,6 +224,10 @@ open class InternalWebviewFragment : ParentFragment() { } } + override fun downloadInternalMedia(mime: String?, url: String?, filename: String?) { + this@InternalWebviewFragment.downloadInternalMedia(mime, url, filename) + } + }) if (savedInstanceState != null) { @@ -231,6 +235,8 @@ open class InternalWebviewFragment : ParentFragment() { } } + open fun downloadInternalMedia(mime: String?, url: String?, filename: String?) = Unit + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) if (shouldLoadUrl) { diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 8ccbe57bc1..f48c6ca568 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1043,6 +1043,10 @@ Downloading Download failed Download successful + Downloading + Download complete + Download Notifications + Canvas notifications for ongoing downloads. Full due date details Submissions diff --git a/libs/pandautils/src/main/AndroidManifest.xml b/libs/pandautils/src/main/AndroidManifest.xml index 03eee83108..c267f2fc1b 100644 --- a/libs/pandautils/src/main/AndroidManifest.xml +++ b/libs/pandautils/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ + () + + init { + createNotificationChannel() + } + + private fun createNotificationChannel() { + if (notificationManager.notificationChannels.any { it.id == CHANNEL_ID }) return + + val name = context.getString(R.string.notificationChannelNameFileDownloadsName) + val description = context.getString(R.string.notificationChannelNameFileDownloadsDescription) + val importance = NotificationManager.IMPORTANCE_HIGH + val channel = NotificationChannel(CHANNEL_ID, name, importance) + channel.description = description + channel.enableVibration(false) + channel.setSound(null, null) + + notificationManager.createNotificationChannel(channel) + } + + fun monitorDownload(downloadId: Long, fileName: String?) { + val job = CoroutineScope(Dispatchers.IO).launch { + try { + monitorDownloadProgress(downloadId, fileName ?: context.getString(R.string.downloadingFile)) + } catch (e: Exception) { + showFailureNotification(downloadId, fileName) + } finally { + activeDownloads.remove(downloadId) + } + } + activeDownloads[downloadId] = job + } + + private suspend fun monitorDownloadProgress(downloadId: Long, fileName: String) { + val notificationId = downloadId.toInt() + var lastProgress = -1 + var isFirstNotification = true + + while (coroutineContext.isActive) { + val query = DownloadManager.Query().setFilterById(downloadId) + val cursor: Cursor? = downloadManager.query(query) + + cursor?.use { + if (it.moveToFirst()) { + val status = it.getInt(it.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) + val bytesDownloaded = it.getLong(it.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) + val bytesTotal = it.getLong(it.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) + + when (status) { + DownloadManager.STATUS_RUNNING, DownloadManager.STATUS_PENDING -> { + val progress = if (bytesTotal > 0) { + ((bytesDownloaded * 100) / bytesTotal).toInt() + } else { + 0 + } + + if (progress != lastProgress || isFirstNotification) { + showProgressNotification(notificationId, fileName, progress, isFirstNotification) + lastProgress = progress + isFirstNotification = false + } + } + + DownloadManager.STATUS_SUCCESSFUL -> { + cancelProgressNotification(notificationId) + showSuccessNotification(downloadId, fileName) + return + } + + DownloadManager.STATUS_FAILED -> { + cancelProgressNotification(notificationId) + showFailureNotification(downloadId, fileName) + return + } + } + } else { + return + } + } + + delay(500) + } + } + + private fun showProgressNotification(notificationId: Int, fileName: String, progress: Int, isHeadsUp: Boolean) { + val notification = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setContentTitle(context.getString(R.string.downloading)) + .setContentText(fileName) + .setProgress(100, progress, false) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setPriority(if (isHeadsUp) NotificationCompat.PRIORITY_HIGH else NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .build() + + notificationManager.notify(notificationId, notification) + } + + private fun showSuccessNotification(downloadId: Long, fileName: String?) { + val notificationId = (downloadId.toInt() + COMPLETION_OFFSET) + + val uri = downloadManager.getUriForDownloadedFile(downloadId) + val mimeType = downloadManager.getMimeTypeForDownloadedFile(downloadId) + + val viewDownloadIntent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, mimeType) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + } + + val pendingIntent = PendingIntent.getActivity( + context, + notificationId, + viewDownloadIntent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + val notification = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setContentTitle(context.getString(R.string.downloadComplete)) + .setContentText(fileName) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_STATUS) + .build() + + notificationManager.notify(notificationId, notification) + } + + private fun showFailureNotification(downloadId: Long, fileName: String?) { + val notificationId = (downloadId.toInt() + COMPLETION_OFFSET) + + val notification = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_sys_warning) + .setContentTitle(context.getString(R.string.downloadFailed)) + .setContentText(fileName) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .build() + + notificationManager.notify(notificationId, notification) + } + + private fun cancelProgressNotification(notificationId: Int) { + notificationManager.cancel(notificationId) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileDownloader.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileDownloader.kt index 95fce1086c..3fb3294fbf 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileDownloader.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileDownloader.kt @@ -25,7 +25,8 @@ import com.instructure.canvasapi2.models.Attachment class FileDownloader( private val context: Context, - private val cookieManager: CookieManager + private val cookieManager: CookieManager, + private val downloadNotificationHelper: DownloadNotificationHelper ) { fun downloadFileToDevice(attachment: Attachment) { downloadFileToDevice(attachment.url, attachment.filename, attachment.contentType) @@ -49,7 +50,7 @@ class FileDownloader( val request = DownloadManager.Request(downloadURI) request - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN) .setTitle(filename) .setMimeType(contentType) @@ -63,6 +64,8 @@ class FileDownloader( request.addRequestHeader("Cookie", cookie) } - downloadManager.enqueue(request) + val downloadId = downloadManager.enqueue(request) + + downloadNotificationHelper.monitorDownload(downloadId, filename) } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt index 5bb251cb32..7d5d2a0bc8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt @@ -67,6 +67,7 @@ import androidx.annotation.ColorRes import androidx.appcompat.app.AppCompatActivity import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.FileProvider +import androidx.core.net.toUri import androidx.core.view.NestedScrollingChild import androidx.core.view.NestedScrollingChildHelper import androidx.core.view.ViewCompat @@ -131,6 +132,8 @@ class CanvasWebView @JvmOverloads constructor( interface MediaDownloadCallback { fun downloadMedia(mime: String?, url: String?, filename: String?) + // Specific to internal file links that are linked directly + fun downloadInternalMedia(mime: String?, url: String?, filename: String?) = Unit } var canvasWebViewClientCallback: CanvasWebViewClientCallback? = null @@ -186,7 +189,11 @@ class CanvasWebView @JvmOverloads constructor( } else { initSettings() setDownloadListener { url, _, contentDisposition, mimetype, _ -> - if (contentDisposition != null) { + // Check if this download was triggered by an internal file link + if (isInternalFileDownloadLink(url)) { + val fileName = parseFileNameFromContentDisposition(contentDisposition, url) + mediaDownloadCallback?.downloadInternalMedia(mimetype, url, fileName) + } else if (contentDisposition != null) { val fileName = parseFileNameFromContentDisposition(contentDisposition, url) canvasWebViewClientCallback?.openMediaFromWebView(mimetype, url, fileName) } @@ -850,6 +857,15 @@ class CanvasWebView @JvmOverloads constructor( } return false } + + fun isInternalFileDownloadLink(url: String): Boolean { + return try { + val uri = url.toUri() + uri.host?.contains("instructure-uploads") == true + } catch (e: Exception) { + false + } + } } } From 3ff18b1e2f40809ebbd00de884c21ff151419ea4 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:58:29 +0200 Subject: [PATCH 46/94] [MBL-19081][All] Dependency update (#3270) Test plan: Smoke test all the apps. refs: MBL-19081 affects: Student, Teacher, Parent release note: none --- apps/build.gradle | 34 ++- .../src/main/java/GlobalDependencies.kt | 64 ++-- apps/gradle.properties | 2 +- apps/parent/build.gradle | 35 ++- apps/parent/proguard-rules.txt | 17 +- .../parentapp/ui/espresso/TestAppManager.kt | 3 +- apps/settings.gradle | 4 +- apps/student/build.gradle | 40 ++- apps/student/proguard-rules.txt | 15 + .../student/espresso/TestAppManager.kt | 3 +- .../student/ui/e2e/classic/GradesE2ETest.kt | 3 +- .../PickerSubmissionUploadInteractionTest.kt | 31 -- .../classic/PickerSubmissionUploadPage.kt | 5 - apps/student/src/main/AndroidManifest.xml | 6 - .../documentscanning/BitmapExtensions.kt | 55 ---- .../DocumentScanningActivity.kt | 109 ------- .../DocumentScanningViewData.kt | 36 --- .../DocumentScanningViewModel.kt | 112 ------- .../itemviewmodels/FilterItemViewModel.kt | 40 --- .../features/files/list/FileListFragment.kt | 3 +- .../AnnotationSubmissionViewModel.kt | 1 + .../PickerSubmissionUploadEffectHandler.kt | 16 - .../picker/PickerSubmissionUploadModels.kt | 2 - .../picker/PickerSubmissionUploadUpdate.kt | 1 - .../picker/ui/PickerSubmissionUploadView.kt | 1 - .../student/util/BaseAppManager.kt | 30 +- .../com/instructure/student/util/FileUtils.kt | 6 - .../res/layout/activity_document_scanning.xml | 84 ----- .../main/res/layout/activity_navigation.xml | 1 + .../fragment_picker_submission_upload.xml | 30 -- .../layout/item_document_scanning_filter.xml | 74 ----- apps/teacher/build.gradle | 59 ++-- apps/teacher/proguard-rules.txt | 17 +- .../teacher/espresso/TestAppManager.kt | 3 +- .../ui/pages/compose/SpeedGraderPage.kt | 34 +-- .../AnnotationCommentViewHolder.kt | 2 +- .../teacher/fragments/FileListFragment.kt | 4 +- .../fragments/SpeedGraderCommentsFragment.kt | 3 +- .../teacher/utils/BaseAppManager.kt | 19 +- .../src/main/res/layout/activity_init.xml | 1 + automation/espresso/build.gradle | 6 +- .../canvas/espresso/TestAppManager.kt | 12 +- automation/soseedygrpc/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- libs/DocumentScanner/build.gradle | 91 ------ .../src/main/AndroidManifest.xml | 25 -- .../documentscanner/ScanActivity.kt | 29 -- .../common/extensions/BitmapExtensions.kt | 51 ---- .../common/extensions/ImageProxyExtensions.kt | 93 ------ .../common/extensions/OpenCvExtensions.kt | 42 --- .../common/extensions/ViewExtensions.kt | 30 -- .../common/utils/FileUriUtils.kt | 240 --------------- .../common/utils/ImageDetectionProperties.kt | 81 ----- .../documentscanner/common/utils/MathUtils.kt | 51 ---- .../common/utils/OpenCvNativeBridge.kt | 247 --------------- .../common/utils/PerspectiveTransformation.kt | 117 ------- .../documentscanner/manager/SessionManager.kt | 68 ----- .../model/DocumentScannerErrorModel.kt | 38 --- .../documentscanner/model/ScannerResults.kt | 28 -- .../documentscanner/ui/DocumentScanner.kt | 46 --- .../documentscanner/ui/base/BaseFragment.kt | 58 ---- .../ui/camerascreen/CameraScreenFragment.kt | 213 ------------- .../ui/components/ProgressView.kt | 38 --- .../ui/components/Quadrilateral.kt | 26 -- .../ui/components/ScanCanvasView.kt | 183 ----------- .../polygon/PolygonPointImageView.kt | 85 ------ .../ui/components/polygon/PolygonView.kt | 183 ----------- .../scansurface/ScanSurfaceListener.kt | 35 --- .../components/scansurface/ScanSurfaceView.kt | 289 ------------------ .../ui/imagecrop/ImageCropFragment.kt | 159 ---------- .../ImageProcessingFragment.kt | 136 --------- .../ui/scan/InternalScanActivity.kt | 196 ------------ .../res/drawable/camera_button_circle.xml | 14 - .../main/res/drawable/crop_corner_circle.xml | 14 - .../src/main/res/drawable/iconclose.png | Bin 2456 -> 0 bytes .../src/main/res/drawable/zdc_flash_off.png | Bin 3263 -> 0 bytes .../src/main/res/drawable/zdc_flash_on.png | Bin 2260 -> 0 bytes .../main/res/drawable/zdc_gallery_icon.png | Bin 3763 -> 0 bytes .../main/res/drawable/zdc_magic_wand_icon.png | Bin 2344 -> 0 bytes .../main/res/drawable/zdc_rotation_icon.png | Bin 3847 -> 0 bytes .../src/main/res/drawable/zdc_tick_icon.png | Bin 5541 -> 0 bytes .../res/layout/fragment_camera_screen.xml | 85 ------ .../main/res/layout/fragment_image_crop.xml | 65 ---- .../res/layout/fragment_image_processing.xml | 80 ----- .../src/main/res/layout/progress_layout.xml | 16 - .../src/main/res/layout/scan_surface_view.xml | 16 - .../src/main/res/values-night/colors.xml | 5 - .../src/main/res/values/colors.xml | 13 - .../src/main/res/values/dimens.xml | 34 --- .../src/main/res/values/ids.xml | 5 - .../src/main/res/values/strings.xml | 7 - .../src/main/res/values/styles.xml | 21 -- libs/annotations/build.gradle | 6 +- .../annotations/PdfSubmissionView.kt | 21 +- libs/canvas-api-2/build.gradle | 6 +- libs/horizon/build.gradle.kts | 9 +- .../account/filepreview/PdfPreview.kt | 2 - libs/interactions/build.gradle | 2 +- libs/login-api-2/build.gradle | 4 +- libs/pandautils/build.gradle | 18 +- .../features/settings/SettingsScreenTest.kt | 15 +- .../room/offline/daos/GradingPeriodDaoTest.kt | 6 +- .../composables/TriStateBottomSheet.kt | 14 +- .../DashboardNotificationsViewModel.kt | 15 +- .../itemviewmodels/UploadItemViewModel.kt | 33 +- .../file/upload/FileUploadDialogFragment.kt | 2 +- .../file/upload/FileUploadDialogParent.kt | 2 +- .../file/upload/FileUploadDialogViewData.kt | 2 +- .../file/upload/FileUploadEventHandler.kt | 2 +- .../file/upload/worker/FileUploadWorker.kt | 2 +- .../features/grades/GradesScreen.kt | 16 +- .../inbox/compose/InboxComposeFragment.kt | 6 +- .../offline/sync/OfflineSyncHelper.kt | 9 +- .../shareextension/ShareExtensionActivity.kt | 2 +- .../ShareExtensionProgressDialogViewModel.kt | 41 +-- .../comments/SpeedGraderCommentsUiState.kt | 2 +- .../comments/SpeedGraderCommentsViewModel.kt | 4 +- .../SpeedGraderCommentLibraryScreen.kt | 1 + .../offline/entities/DiscussionTopicEntity.kt | 4 +- .../services/NotoriousUploadWorker.kt | 2 +- .../layout/view_canvas_web_view_wrapper.xml | 2 +- .../DashboardNotificationsViewModelTest.kt | 19 +- .../offline/sync/OfflineSyncHelperTest.kt | 47 +-- .../ShareExtensionProgressViewModelTest.kt | 174 +++++------ 124 files changed, 507 insertions(+), 4263 deletions(-) delete mode 100644 apps/student/src/main/java/com/instructure/student/features/documentscanning/BitmapExtensions.kt delete mode 100644 apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningActivity.kt delete mode 100644 apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewData.kt delete mode 100644 apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewModel.kt delete mode 100644 apps/student/src/main/java/com/instructure/student/features/documentscanning/itemviewmodels/FilterItemViewModel.kt delete mode 100644 apps/student/src/main/res/layout/activity_document_scanning.xml delete mode 100644 apps/student/src/main/res/layout/item_document_scanning_filter.xml delete mode 100644 libs/DocumentScanner/build.gradle delete mode 100644 libs/DocumentScanner/src/main/AndroidManifest.xml delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ScanActivity.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/BitmapExtensions.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ImageProxyExtensions.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/OpenCvExtensions.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ViewExtensions.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/FileUriUtils.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/ImageDetectionProperties.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/MathUtils.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/OpenCvNativeBridge.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/PerspectiveTransformation.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/manager/SessionManager.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/DocumentScannerErrorModel.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/ScannerResults.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/DocumentScanner.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/base/BaseFragment.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/camerascreen/CameraScreenFragment.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ProgressView.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/Quadrilateral.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ScanCanvasView.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonPointImageView.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonView.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceListener.kt delete mode 100755 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceView.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imagecrop/ImageCropFragment.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imageprocessing/ImageProcessingFragment.kt delete mode 100644 libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/scan/InternalScanActivity.kt delete mode 100644 libs/DocumentScanner/src/main/res/drawable/camera_button_circle.xml delete mode 100644 libs/DocumentScanner/src/main/res/drawable/crop_corner_circle.xml delete mode 100644 libs/DocumentScanner/src/main/res/drawable/iconclose.png delete mode 100644 libs/DocumentScanner/src/main/res/drawable/zdc_flash_off.png delete mode 100644 libs/DocumentScanner/src/main/res/drawable/zdc_flash_on.png delete mode 100644 libs/DocumentScanner/src/main/res/drawable/zdc_gallery_icon.png delete mode 100644 libs/DocumentScanner/src/main/res/drawable/zdc_magic_wand_icon.png delete mode 100644 libs/DocumentScanner/src/main/res/drawable/zdc_rotation_icon.png delete mode 100644 libs/DocumentScanner/src/main/res/drawable/zdc_tick_icon.png delete mode 100644 libs/DocumentScanner/src/main/res/layout/fragment_camera_screen.xml delete mode 100644 libs/DocumentScanner/src/main/res/layout/fragment_image_crop.xml delete mode 100644 libs/DocumentScanner/src/main/res/layout/fragment_image_processing.xml delete mode 100644 libs/DocumentScanner/src/main/res/layout/progress_layout.xml delete mode 100644 libs/DocumentScanner/src/main/res/layout/scan_surface_view.xml delete mode 100644 libs/DocumentScanner/src/main/res/values-night/colors.xml delete mode 100644 libs/DocumentScanner/src/main/res/values/colors.xml delete mode 100644 libs/DocumentScanner/src/main/res/values/dimens.xml delete mode 100644 libs/DocumentScanner/src/main/res/values/ids.xml delete mode 100644 libs/DocumentScanner/src/main/res/values/strings.xml delete mode 100644 libs/DocumentScanner/src/main/res/values/styles.xml diff --git a/apps/build.gradle b/apps/build.gradle index a0b3f6aaa8..5d6de59820 100644 --- a/apps/build.gradle +++ b/apps/build.gradle @@ -34,6 +34,7 @@ buildscript { classpath Plugins.FIREBASE_CRASHLYTICS if (project.coverageEnabled) { classpath Plugins.JACOCO_ANDROID } classpath Plugins.HILT + classpath Plugins.KSP } } @@ -50,10 +51,40 @@ allprojects { username pspdfMavenUser password pspdfMavenPass } - url 'https://customers.pspdfkit.com/maven/' + url 'https://my.nutrient.io/maven' } maven { url "https://maven.google.com/" } } + + plugins.withType(com.android.build.gradle.BasePlugin) { + android { + packaging { + resources { + pickFirsts += [ + 'META-INF/INDEX.LIST', + 'META-INF/io.netty.versions.properties' + ] + merges += [ + 'META-INF/LICENSE*', + 'META-INF/NOTICE*', + 'META-INF/DEPENDENCIES*' + ] + excludes += [ + 'META-INF/DEPENDENCIES', + 'META-INF/LICENSE', + 'META-INF/LICENSE.txt', + 'META-INF/LICENSE.md', + 'META-INF/NOTICE', + 'META-INF/NOTICE.txt', + 'META-INF/NOTICE.md', + 'META-INF/maven/**', + 'META-INF/*.kotlin_module', + 'META-INF/services/javax.annotation.processing.Processor' + ] + } + } + } + } } task assembleAllApps() { @@ -72,4 +103,3 @@ configurations.all{ } } } - diff --git a/apps/buildSrc/src/main/java/GlobalDependencies.kt b/apps/buildSrc/src/main/java/GlobalDependencies.kt index a10251eb0d..d922ebaa3c 100644 --- a/apps/buildSrc/src/main/java/GlobalDependencies.kt +++ b/apps/buildSrc/src/main/java/GlobalDependencies.kt @@ -8,7 +8,7 @@ object Versions { /* Build/tooling */ const val ANDROID_GRADLE_TOOLS = "8.6.1" - const val BUILD_TOOLS = "34.0.0" + const val BUILD_TOOLS = "35.0.0" /* Testing */ const val JUNIT = "4.13.2" @@ -18,31 +18,31 @@ object Versions { /* Kotlin */ const val KOTLIN = "2.0.21" const val KOTLIN_COROUTINES = "1.9.0" + const val KSP = "2.0.21-1.0.27" /* Google, Play Services */ - const val GOOGLE_SERVICES = "4.4.2" + const val GOOGLE_SERVICES = "4.4.3" /* Others */ - const val APOLLO = "4.1.1" - const val PSPDFKIT = "2024.3.1" + const val APOLLO = "4.3.3" + const val NUTRIENT = "10.7.0" const val PHOTO_VIEW = "2.3.0" const val MOBIUS = "1.2.1" - const val HILT = "2.52" - const val HILT_ANDROIDX = "1.2.0" - const val LIFECYCLE = "2.8.6" - const val FRAGMENT = "1.8.4" - const val WORK_MANAGER = "2.9.1" - const val WORK_TEST = "2.9.1" - const val GLIDE_VERSION = "4.16.0" + const val HILT = "2.57.2" + const val HILT_ANDROIDX = "1.3.0" + const val LIFECYCLE = "2.9.4" + const val FRAGMENT = "1.8.9" + const val WORK_MANAGER = "2.10.5" + const val GLIDE_VERSION = "5.0.5" const val RETROFIT = "2.11.0" const val OKHTTP = "4.12.0" - const val ROOM = "2.6.1" - const val HAMCREST = "2.2" - const val NAVIGATION = "2.8.3" - const val MEDIA3 = "1.6.1" - const val DATASTORE = "1.1.1" - const val LOTTIE = "6.5.2" - const val ENCRYPTED_SHARED_PREFERENCES = "1.0.0" + const val ROOM = "2.7.0" + const val HAMCREST = "3.0" + const val NAVIGATION = "2.9.5" + const val MEDIA3 = "1.8.0" + const val DATASTORE = "1.1.7" + const val LOTTIE = "6.6.6" + const val ENCRYPTED_SHARED_PREFERENCES = "1.1.0" const val JAVA_JWT = "4.5.0" const val GLANCE = "1.1.1" const val LIVEDATA = "1.9.0" @@ -65,28 +65,27 @@ object Libs { const val ANDROIDX_APPCOMPAT = "androidx.appcompat:appcompat:1.7.0" const val ANDROIDX_BROWSER = "androidx.browser:browser:1.8.0" const val ANDROIDX_CARDVIEW = "androidx.cardview:cardview:1.0.0" - const val ANDROIDX_CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:2.1.4" + const val ANDROIDX_CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:2.2.0" const val ANDROIDX_EXIF = "androidx.exifinterface:exifinterface:1.3.7" const val ANDROIDX_FRAGMENT = "androidx.fragment:fragment:${Versions.FRAGMENT}" const val ANDROIDX_FRAGMENT_KTX = "androidx.fragment:fragment-ktx:${Versions.FRAGMENT}" const val ANDROIDX_PALETTE = "androidx.palette:palette:1.0.0" const val ANDROIDX_PERCENT = "androidx.percentlayout:percentlayout:1.0.0" - const val ANDROIDX_RECYCLERVIEW = "androidx.recyclerview:recyclerview:1.3.2" + const val ANDROIDX_RECYCLERVIEW = "androidx.recyclerview:recyclerview:1.4.0" const val ANDROIDX_VECTOR = "androidx.vectordrawable:vectordrawable:1.2.0" const val ANDROIDX_SWIPE_REFRESH_LAYOUT = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" const val ANDROIDX_CORE_TESTING = "androidx.arch.core:core-testing:2.2.0" const val ANDROIDX_WORK_MANAGER = "androidx.work:work-runtime:${Versions.WORK_MANAGER}" const val ANDROIDX_WORK_MANAGER_KTX = "androidx.work:work-runtime-ktx:${Versions.WORK_MANAGER}" - const val ANDROIDX_WORK_TEST = "androidx.work:work-testing:${Versions.WORK_TEST}" - const val ANDROIDX_WEBKIT = "androidx.webkit:webkit:1.9.0" - const val ANDROIDX_DATABINDING_COMPILER = "androidx.databinding:databinding-compiler:${Versions.ANDROID_GRADLE_TOOLS}" // This is bundled with the gradle plugin so we use the same version - const val ANDROIDX_COMPOSE_ACTIVITY = "androidx.activity:activity-compose:1.9.0" + const val ANDROIDX_WORK_TEST = "androidx.work:work-testing:${Versions.WORK_MANAGER}" + const val ANDROIDX_WEBKIT = "androidx.webkit:webkit:1.12.0" + const val ANDROIDX_COMPOSE_ACTIVITY = "androidx.activity:activity-compose:1.10.0" const val DATASTORE = "androidx.datastore:datastore-preferences:${Versions.DATASTORE}" const val ENCRYPTED_SHARED_PREFERENCES = "androidx.security:security-crypto:${Versions.ENCRYPTED_SHARED_PREFERENCES}" const val JAVA_JWT = "com.auth0:java-jwt:${Versions.JAVA_JWT}" /* Firebase */ - const val FIREBASE_BOM = "com.google.firebase:firebase-bom:33.4.0" + const val FIREBASE_BOM = "com.google.firebase:firebase-bom:34.3.0" const val FIREBASE_CRASHLYTICS = "com.google.firebase:firebase-crashlytics" const val FIREBASE_MESSAGING = "com.google.firebase:firebase-messaging" const val FIREBASE_CONFIG = "com.google.firebase:firebase-config" @@ -95,7 +94,7 @@ object Libs { /* Google Dependencies */ const val PLAY_IN_APP_UPDATES = "com.google.android.play:app-update:2.1.0" const val FLEXBOX_LAYOUT = "com.google.android.flexbox:flexbox:3.0.0" - const val MATERIAL_DESIGN = "com.google.android.material:material:1.12.0" + const val MATERIAL_DESIGN = "com.google.android.material:material:1.13.0" /* Mobius */ const val MOBIUS_CORE = "com.spotify.mobius:mobius-core:${Versions.MOBIUS}" @@ -133,7 +132,7 @@ object Libs { const val COMPOSE_VIEW_MODEL = "androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.LIFECYCLE}" const val COMPOSE_NAVIGATION = "androidx.navigation:navigation-compose:2.8.9" /* Media and content handling */ - const val PSPDFKIT = "com.pspdfkit:pspdfkit:${Versions.PSPDFKIT}" + const val NUTRIENT = "io.nutrient:nutrient:${Versions.NUTRIENT}" const val MEDIA3 = "androidx.media3:media3-exoplayer:${Versions.MEDIA3}" const val MEDIA3_UI = "androidx.media3:media3-ui:${Versions.MEDIA3}" const val MEDIA3_HLS = "androidx.media3:media3-exoplayer-hls:${Versions.MEDIA3}" @@ -171,7 +170,7 @@ object Libs { const val APACHE_COMMONS_TEXT = "org.apache.commons:commons-text:1.12.0" const val CAMERA_VIEW = "com.otaliastudios:cameraview:2.7.2" - const val PENDO = "sdk.pendo.io:pendoIO:3.6+" + const val PENDO = "sdk.pendo.io:pendoIO:3.7.+" const val ROOM = "androidx.room:room-runtime:${Versions.ROOM}" const val ROOM_COMPILER = "androidx.room:room-compiler:${Versions.ROOM}" @@ -183,7 +182,7 @@ object Libs { const val RRULE = "org.scala-saddle:google-rfc-2445:20110304" // Compose - const val COMPOSE_BOM = "androidx.compose:compose-bom:2024.09.02" + const val COMPOSE_BOM = "androidx.compose:compose-bom:2025.09.01" const val COMPOSE_MATERIAL = "androidx.compose.material:material" const val COMPOSE_MATERIAL_ICONS = "androidx.compose.material:material-icons-core" const val COMPOSE_PREVIEW = "androidx.compose.ui:ui-tooling-preview" @@ -191,11 +190,11 @@ object Libs { const val COMPOSE_UI = "androidx.compose.ui:ui-android" const val COMPOSE_UI_TEST = "androidx.compose.ui:ui-test-junit4" const val COMPOSE_UI_TEST_MANIFEST = "androidx.compose.ui:ui-test-manifest" - const val COMPOSE_MATERIAL_3 = "androidx.compose.material3:material3:1.4.0-alpha12" + const val COMPOSE_MATERIAL_3 = "androidx.compose.material3:material3:1.4.0" const val COMPOSE_ADAPTIVE = "androidx.compose.material3.adaptive:adaptive" const val COMPOSE_MATERIAL3_WINDOW_SIZE = "androidx.compose.material3:material3-window-size-class" - const val COMPOSE_NAVIGATION_HILT = "androidx.hilt:hilt-navigation-compose:1.2.0" - const val COMPOSE_FRAGMENT = "androidx.fragment:fragment-compose:1.8.6" + const val COMPOSE_NAVIGATION_HILT = "androidx.hilt:hilt-navigation-compose:1.3.0" + const val COMPOSE_FRAGMENT = "androidx.fragment:fragment-compose:1.8.9" // Glance const val GLANCE = "androidx.glance:glance:${Versions.GLANCE}" @@ -220,4 +219,5 @@ object Plugins { const val GOOGLE_SERVICES = "com.google.gms:google-services:${Versions.GOOGLE_SERVICES}" const val JACOCO_ANDROID = "com.dicedmelon.gradle:jacoco-android:${Versions.JACOCO_ANDROID}" const val HILT = "com.google.dagger:hilt-android-gradle-plugin:${Versions.HILT}" + const val KSP = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:${Versions.KSP}" } diff --git a/apps/gradle.properties b/apps/gradle.properties index 61364d13a3..01ce297dfc 100644 --- a/apps/gradle.properties +++ b/apps/gradle.properties @@ -3,4 +3,4 @@ android.enableJetifier=true android.nonFinalResIds=false android.nonTransitiveRClass=false android.useAndroidX=true -org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 \ No newline at end of file +org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 \ No newline at end of file diff --git a/apps/parent/build.gradle b/apps/parent/build.gradle index d8c7168f16..1736c5549c 100644 --- a/apps/parent/build.gradle +++ b/apps/parent/build.gradle @@ -19,7 +19,8 @@ plugins { id 'com.android.application' id 'com.google.gms.google-services' id 'org.jetbrains.kotlin.android' - id 'kotlin-kapt' + id 'kotlin-kapt' // Keep kapt for Data Binding + id 'com.google.devtools.ksp' id 'com.google.firebase.crashlytics' id 'dagger.hilt.android.plugin' id 'org.jetbrains.kotlin.plugin.compose' @@ -51,15 +52,23 @@ android { PrivateData.merge(project, "parent") } - packagingOptions { - exclude 'META-INF/maven/com.google.guava/guava/pom.xml' - exclude 'META-INF/maven/com.google.guava/guava/pom.properties' - exclude 'META-INF/DEPENDENCIES' - exclude 'META-INF/LICENSE' - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE' - exclude 'META-INF/rxjava.properties' - exclude 'LICENSE.txt' + packaging { + resources { + pickFirsts += [ + 'META-INF/INDEX.LIST', + 'META-INF/io.netty.versions.properties' + ] + excludes += [ + 'META-INF/DEPENDENCIES', + 'META-INF/LICENSE', + 'META-INF/LICENSE.txt', + 'META-INF/NOTICE', + 'META-INF/NOTICE.txt', + 'META-INF/maven/com.google.guava/guava/pom.properties', + 'META-INF/maven/com.google.guava/guava/pom.xml', + 'META-INF/rxjava.properties' + ] + } } @@ -194,14 +203,14 @@ dependencies { /* DI */ implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER implementation Libs.HILT_ANDROIDX_WORK - kapt Libs.HILT_ANDROIDX_COMPILER + ksp Libs.HILT_ANDROIDX_COMPILER androidTestImplementation Libs.HILT_TESTING /* ROOM */ implementation Libs.ROOM - kapt Libs.ROOM_COMPILER + ksp Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES /* Navigation */ diff --git a/apps/parent/proguard-rules.txt b/apps/parent/proguard-rules.txt index bde42b7512..ab46a57d84 100644 --- a/apps/parent/proguard-rules.txt +++ b/apps/parent/proguard-rules.txt @@ -254,4 +254,19 @@ -dontwarn java.beans.SimpleBeanInfo -keep class androidx.navigation.** { *; } - -keep interface androidx.navigation.** { *; } \ No newline at end of file + -keep interface androidx.navigation.** { *; } + +# Netty and BlockHound integration +-dontwarn reactor.blockhound.integration.BlockHoundIntegration +-dontwarn io.netty.util.internal.Hidden$NettyBlockHoundIntegration +-keep class reactor.blockhound.integration.** { *; } +-keep class io.netty.util.internal.Hidden$NettyBlockHoundIntegration { *; } + +# Additional Netty keep rules for R8 +-dontwarn io.netty.** +-keep class io.netty.** { *; } +-keepclassmembers class io.netty.** { *; } + +# BlockHound related classes +-dontwarn reactor.blockhound.** +-keep class reactor.blockhound.** { *; } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/TestAppManager.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/TestAppManager.kt index 5677669b65..7f637c07c3 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/TestAppManager.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/TestAppManager.kt @@ -17,6 +17,7 @@ package com.instructure.parentapp.ui.espresso +import androidx.work.DefaultWorkerFactory import androidx.work.WorkerFactory import com.instructure.pandautils.features.reminder.AlarmScheduler import com.instructure.parentapp.util.BaseAppManager @@ -26,7 +27,7 @@ open class TestAppManager : BaseAppManager() { private var workerFactory: WorkerFactory? = null override fun getWorkManagerFactory(): WorkerFactory { - return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() + return workerFactory ?: DefaultWorkerFactory } override fun getScheduler(): AlarmScheduler? { diff --git a/apps/settings.gradle b/apps/settings.gradle index 6be9862e0f..99bd1d48a7 100644 --- a/apps/settings.gradle +++ b/apps/settings.gradle @@ -7,7 +7,7 @@ pluginManagement { } } dependencies { - classpath("com.android.tools:r8:8.2.47") + classpath("com.android.tools:r8:8.13.6") } } } @@ -29,7 +29,6 @@ include ':pandautils' include ':rceditor' include ':recyclerview' include ':pandares' -include ':DocumentScanner' include ':horizon' project(':annotations').projectDir = new File(rootProject.projectDir, '/../libs/annotations') @@ -43,5 +42,4 @@ project(':pandautils').projectDir = new File(rootProject.projectDir, '/../libs/p project(':rceditor').projectDir = new File(rootProject.projectDir, '/../libs/rceditor') project(':recyclerview').projectDir = new File(rootProject.projectDir, '/../libs/recyclerview') project(':pandares').projectDir = new File(rootProject.projectDir, '/../libs/pandares') -project(':DocumentScanner').projectDir = new File(rootProject.projectDir, '/../libs/DocumentScanner') project(':horizon').projectDir = new File(rootProject.projectDir, '/../libs/horizon') diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 9c288dbd49..0f87999743 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -16,7 +16,8 @@ */ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-kapt' // Keep kapt for Data Binding +apply plugin: 'com.google.devtools.ksp' apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'org.jetbrains.kotlin.plugin.compose' @@ -61,15 +62,23 @@ android { } } - packagingOptions { - exclude 'META-INF/maven/com.google.guava/guava/pom.xml' - exclude 'META-INF/maven/com.google.guava/guava/pom.properties' - exclude 'META-INF/DEPENDENCIES' - exclude 'META-INF/LICENSE' - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE' - exclude 'META-INF/rxjava.properties' - exclude 'LICENSE.txt' + packaging { + resources { + pickFirsts += [ + 'META-INF/INDEX.LIST', + 'META-INF/io.netty.versions.properties' + ] + excludes += [ + 'META-INF/DEPENDENCIES', + 'META-INF/LICENSE', + 'META-INF/LICENSE.txt', + 'META-INF/NOTICE', + 'META-INF/NOTICE.txt', + 'META-INF/maven/com.google.guava/guava/pom.properties', + 'META-INF/maven/com.google.guava/guava/pom.xml', + 'META-INF/rxjava.properties' + ] + } } lintOptions { @@ -227,7 +236,6 @@ dependencies { implementation project(path: ':annotations') implementation project(path: ':rceditor') implementation project(path: ':interactions') - implementation project(path: ':DocumentScanner') implementation project(path: ':horizon') /* Android Test Dependencies */ @@ -294,15 +302,15 @@ dependencies { implementation Libs.LIVE_DATA implementation Libs.VIEW_MODE_SAVED_STATE implementation Libs.ANDROIDX_FRAGMENT_KTX - kapt Libs.LIFECYCLE_COMPILER + kapt Libs.LIFECYCLE_COMPILER // Keep kapt for lifecycle if it includes Data Binding /* DI */ implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER androidTestImplementation Libs.HILT_TESTING - kaptAndroidTestQa Libs.HILT_TESTING_COMPILER + kspAndroidTest Libs.HILT_TESTING_COMPILER implementation Libs.HILT_ANDROIDX_WORK - kapt Libs.HILT_ANDROIDX_COMPILER + ksp Libs.HILT_ANDROIDX_COMPILER androidTestImplementation Libs.UI_AUTOMATOR @@ -314,7 +322,7 @@ dependencies { /* ROOM */ implementation Libs.ROOM - kapt Libs.ROOM_COMPILER + ksp Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES testImplementation Libs.HAMCREST diff --git a/apps/student/proguard-rules.txt b/apps/student/proguard-rules.txt index aec6327015..90370642f2 100644 --- a/apps/student/proguard-rules.txt +++ b/apps/student/proguard-rules.txt @@ -269,3 +269,18 @@ -keep class androidx.navigation.** { *; } -keep interface androidx.navigation.** { *; } + +# Netty and BlockHound integration +-dontwarn reactor.blockhound.integration.BlockHoundIntegration +-dontwarn io.netty.util.internal.Hidden$NettyBlockHoundIntegration +-keep class reactor.blockhound.integration.** { *; } +-keep class io.netty.util.internal.Hidden$NettyBlockHoundIntegration { *; } + +# Additional Netty keep rules for R8 +-dontwarn io.netty.** +-keep class io.netty.** { *; } +-keepclassmembers class io.netty.** { *; } + +# BlockHound related classes +-dontwarn reactor.blockhound.** +-keep class reactor.blockhound.** { *; } diff --git a/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt b/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt index fc00cc637e..3b21a05d1d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt @@ -16,6 +16,7 @@ */ package com.instructure.student.espresso +import androidx.work.DefaultWorkerFactory import androidx.work.WorkerFactory import com.instructure.student.util.BaseAppManager @@ -24,6 +25,6 @@ open class TestAppManager : BaseAppManager() { var workerFactory: WorkerFactory? = null override fun getWorkManagerFactory(): WorkerFactory { - return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() + return workerFactory ?: DefaultWorkerFactory } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt index 2669a7eb0e..8114133b74 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt @@ -81,7 +81,8 @@ class GradesE2ETest: StudentComposeTest() { courseGradesPage.assertItemDisplayed(quizMatcher) courseGradesPage.assertGradeNotDisplayed(quizMatcher) - val dueDateInCanvasFormat = getDateInCanvasCalendarFormat(1.days.fromNow.iso8601) + var dueDateInCanvasFormat = getDateInCanvasCalendarFormat(1.days.fromNow.iso8601) + dueDateInCanvasFormat = dueDateInCanvasFormat.replace(" 0", " ") Log.d(ASSERTION_TAG, "Assert that the '${assignment.name}' assignment's due date is tomorrow ('$dueDateInCanvasFormat').") courseGradesPage.assertAssignmentDueDate(assignment.name, dueDateInCanvasFormat) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt index 86ac820578..0620ff5dfa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt @@ -190,37 +190,6 @@ class PickerSubmissionUploadInteractionTest : StudentComposeTest() { pickerSubmissionUploadPage.assertFileDisplayed(mockedFileName) } - @Test - @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) - fun testFab_scanner(){ - val scannerComponent = "com.instructure.student.features.documentscanning.DocumentScanningActivity" - - goToSubmissionPicker() - - Intents.init() - try { - val context = getInstrumentation().targetContext - val dir = context.externalCacheDir - val sampleFile = File(dir, mockedFileName) - val uri = Uri.fromFile(sampleFile) - val resultData = Intent().apply { data = uri } - val scannerResult = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) - - intending( - IntentMatchers.hasComponent(scannerComponent) - ).respondWith(scannerResult) - - pickerSubmissionUploadPage.chooseScanner() - } finally { - release() - } - - pickerSubmissionUploadPage.waitForSubmitButtonToAppear() - - pickerSubmissionUploadPage.assertFileDisplayed(mockedFileName) - - } - @Test @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) fun testDeleteFile() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt index 97d3136adf..3c96301b25 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt @@ -33,7 +33,6 @@ class PickerSubmissionUploadPage : BasePage(R.id.pickerSubmissionUploadPage) { private val deviceIcon by OnViewWithId(R.id.sourceDeviceIcon) private val cameraIcon by OnViewWithId(R.id.sourceCameraIcon) private val galleryIcon by OnViewWithId(R.id.sourceGalleryIcon) - private val scannerIcon by OnViewWithId(R.id.sourceDocumentScanningIcon) private val deleteButton by OnViewWithId(R.id.deleteButton) fun chooseDevice() { @@ -48,10 +47,6 @@ class PickerSubmissionUploadPage : BasePage(R.id.pickerSubmissionUploadPage) { galleryIcon.click() } - fun chooseScanner() { - scannerIcon.click() - } - fun waitForSubmitButtonToAppear() { waitForViewWithText(R.string.submit) } diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml index 6cc7ffea16..af986c7a9d 100644 --- a/apps/student/src/main/AndroidManifest.xml +++ b/apps/student/src/main/AndroidManifest.xml @@ -286,12 +286,6 @@ - - diff --git a/apps/student/src/main/java/com/instructure/student/features/documentscanning/BitmapExtensions.kt b/apps/student/src/main/java/com/instructure/student/features/documentscanning/BitmapExtensions.kt deleted file mode 100644 index a4a8cd2a98..0000000000 --- a/apps/student/src/main/java/com/instructure/student/features/documentscanning/BitmapExtensions.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2022 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.instructure.student.features.documentscanning - -import android.graphics.* - -fun Bitmap.toGrayscale(): Bitmap { - val width = this.width - val height = this.height - - val grayscaleBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(grayscaleBitmap) - val paint = Paint() - val colorMatrix = ColorMatrix() - colorMatrix.setSaturation(0f) - val colorMatrixFilter = ColorMatrixColorFilter(colorMatrix) - paint.colorFilter = colorMatrixFilter - canvas.drawBitmap(this, 0f, 0f, paint) - return grayscaleBitmap -} - -fun Bitmap.toMonochrome(): Bitmap { - val width = this.width - val height = this.height - - val monochromeBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - - val hsv = FloatArray(3) - for (column in 0 until width) { - for (row in 0 until height) { - Color.colorToHSV(this.getPixel(column, row), hsv) - if (hsv[2] > 0.5f) { - monochromeBitmap.setPixel(column, row, Color.WHITE) - } else { - monochromeBitmap.setPixel(column, row, Color.BLACK) - } - } - } - - return monochromeBitmap -} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningActivity.kt b/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningActivity.kt deleted file mode 100644 index 73badd284b..0000000000 --- a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningActivity.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2022 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.instructure.student.features.documentscanning - -import android.app.Activity -import android.content.Intent -import android.graphics.Bitmap -import android.os.Bundle -import androidx.activity.viewModels -import androidx.core.content.ContextCompat -import androidx.core.net.toUri -import androidx.databinding.DataBindingUtil -import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.student.R -import com.instructure.student.databinding.ActivityDocumentScanningBinding -import com.zynksoftware.documentscanner.ScanActivity -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.model.ScannerResults -import dagger.hilt.android.AndroidEntryPoint -import java.io.File -import java.io.FileOutputStream -import java.text.SimpleDateFormat -import java.util.* - -@AndroidEntryPoint -class DocumentScanningActivity : ScanActivity() { - - private lateinit var binding: ActivityDocumentScanningBinding - - private val viewModel: DocumentScanningViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = DataBindingUtil.setContentView(this, R.layout.activity_document_scanning) - binding.lifecycleOwner = this - binding.viewModel = viewModel - - addFragmentContentLayout() - - setupToolbar() - - viewModel.events.observe(this) { event -> - event.getContentIfNotHandled()?.let { - handleAction(it) - } - } - } - - private fun handleAction(action: DocumentScanningAction) { - when (action) { - is DocumentScanningAction.SaveBitmapAction -> { - val file = File(filesDir, "scanned_${SimpleDateFormat("yyyyMMddkkmmss", Locale.getDefault()).format(Date())}.jpg") - var fileOutputStream: FileOutputStream? = null - try { - fileOutputStream = FileOutputStream(file.absolutePath) - action.bitmap.compress(Bitmap.CompressFormat.JPEG, action.quality, fileOutputStream) - val intent = Intent() - intent.data = file.toUri() - setResult(Activity.RESULT_OK, intent) - finish() - } finally { - fileOutputStream?.run { - flush() - close() - } - } - } - } - } - - override fun onClose() { - setResult(RESULT_CANCELED) - finish() - } - - override fun onError(error: DocumentScannerErrorModel) { - - } - - override fun onSuccess(scannerResults: ScannerResults) { - viewModel.setScannerResults(scannerResults) - } - - private fun setupToolbar() { - binding.toolbar.apply { - setTitle(R.string.documentScanningTitle) - navigationIcon = ContextCompat.getDrawable(this@DocumentScanningActivity, R.drawable.ic_back_arrow) - navigationIcon?.isAutoMirrored = true - ViewStyler.themeToolbarLight(this@DocumentScanningActivity, this) - setNavigationContentDescription(R.string.close) - setNavigationOnClickListener { onClose() } - } - } -} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewData.kt b/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewData.kt deleted file mode 100644 index 7db75cde27..0000000000 --- a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewData.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2022 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.instructure.student.features.documentscanning - -import android.graphics.Bitmap -import androidx.databinding.BaseObservable -import androidx.databinding.Bindable -import com.instructure.student.features.documentscanning.itemviewmodels.FilterItemViewModel - -data class DocumentScanningViewData( - @get:Bindable var selectedBitmap: Bitmap, - val filterItemViewModels: List -) : BaseObservable() - -data class FilterItemViewData( - val bitmap: Bitmap, - val name: String -) - -sealed class DocumentScanningAction { - data class SaveBitmapAction(val bitmap: Bitmap, val quality: Int): DocumentScanningAction() -} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewModel.kt deleted file mode 100644 index 7a99039541..0000000000 --- a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewModel.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2022 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.instructure.student.features.documentscanning - -import android.content.res.Resources -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.instructure.pandautils.R -import com.instructure.pandautils.BR -import com.instructure.student.features.documentscanning.itemviewmodels.FilterItemViewModel -import com.instructure.pandautils.mvvm.Event -import com.instructure.pandautils.mvvm.ViewState -import com.zynksoftware.documentscanner.model.ScannerResults -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class DocumentScanningViewModel @Inject constructor( - private val resources: Resources -) : ViewModel() { - - val state: LiveData - get() = _state - private val _state = MutableLiveData() - - val data: LiveData - get() = _data - private val _data = MutableLiveData() - - val events: LiveData> - get() = _events - private val _events = MutableLiveData>() - - private lateinit var selectedItem: FilterItemViewModel - - fun setScannerResults(results: ScannerResults) { - _state.postValue(ViewState.Loading) - createViewData(results) - } - - private fun createViewData(results: ScannerResults) { - if (results.croppedImageFile != null && results.originalImageFile != null) { - val croppedBitmap = BitmapFactory.decodeFile(results.croppedImageFile!!.path) - val originalBitmap = BitmapFactory.decodeFile(results.originalImageFile!!.path) - val grayscaleBitmap = croppedBitmap.toGrayscale() - val monochromeBitmap = croppedBitmap.toMonochrome() - - //We no longer need these files - results.croppedImageFile?.delete() - results.croppedImageFile?.delete() - results.transformedImageFile?.delete() - - val filters = listOf( - createFilterViewModel(croppedBitmap, true, resources.getString(R.string.filter_name_color)), - createFilterViewModel(grayscaleBitmap, false, resources.getString(R.string.filter_name_grayscale)), - createFilterViewModel(monochromeBitmap, false, resources.getString(R.string.filter_name_monochrome)), - createFilterViewModel(originalBitmap, false, resources.getString(R.string.filter_name_original)) - ) - selectedItem = filters[0] - - val viewData = DocumentScanningViewData( - croppedBitmap, - filters - ) - _data.postValue(viewData) - _state.postValue(ViewState.Success) - } else { - _state.postValue(ViewState.Error()) - } - } - - private fun createFilterViewModel(bitmap: Bitmap, selected: Boolean, name: String): FilterItemViewModel { - return FilterItemViewModel( - FilterItemViewData(bitmap, name), - selected, - this::onFilterSelected - ) - } - - fun onFilterSelected(itemViewModel: FilterItemViewModel) { - selectedItem.apply { - selected = false - notifyPropertyChanged(BR.selected) - } - _data.value?.apply { - selectedBitmap = itemViewModel.data.bitmap - notifyPropertyChanged(BR.selectedBitmap) - } - selectedItem = itemViewModel - } - - fun onSaveClicked() { - _events.postValue(Event(DocumentScanningAction.SaveBitmapAction(selectedItem.data.bitmap, 100))) - } -} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/documentscanning/itemviewmodels/FilterItemViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/documentscanning/itemviewmodels/FilterItemViewModel.kt deleted file mode 100644 index c84895a93e..0000000000 --- a/apps/student/src/main/java/com/instructure/student/features/documentscanning/itemviewmodels/FilterItemViewModel.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2022 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.instructure.student.features.documentscanning.itemviewmodels - -import androidx.databinding.BaseObservable -import androidx.databinding.Bindable -import com.instructure.pandautils.BR -import com.instructure.student.features.documentscanning.FilterItemViewData -import com.instructure.pandautils.mvvm.ItemViewModel -import com.instructure.student.R - -class FilterItemViewModel( - val data: FilterItemViewData, - @get:Bindable var selected: Boolean, - val onSelect: (FilterItemViewModel) -> Unit -) : ItemViewModel, BaseObservable() { - override val layoutId: Int = R.layout.item_document_scanning_filter - - fun select() { - if (!selected) { - selected = true - notifyPropertyChanged(BR.selected) - onSelect(this) - } - } -} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt index df6e22f475..c6fd5a2a59 100644 --- a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt @@ -551,8 +551,9 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent } } - override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { + override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { workInfoLiveData.observe(viewLifecycleOwner) { + if (it == null) return@observe if (it.state == WorkInfo.State.SUCCEEDED) { updateFileList(true) folder?.let { fileFolder -> diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModel.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModel.kt index 57cb35e018..9d09c607ff 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModel.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModel.kt @@ -59,6 +59,7 @@ class AnnotationSubmissionViewModel @Inject constructor( ViewState.Error(resources.getString(R.string.failedToLoadSubmission)) } } catch (e: Exception) { + e.printStackTrace() _state.value = ViewState.Error(resources.getString(R.string.failedToLoadSubmission)) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt index f99647fdac..726b08dbce 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt @@ -18,7 +18,6 @@ package com.instructure.student.mobius.assignmentDetails.submission.picker import android.app.Activity import android.content.Context -import android.content.Intent import android.net.Uri import androidx.core.content.FileProvider import com.instructure.canvasapi2.models.postmodels.FileSubmitObject @@ -32,7 +31,6 @@ import com.instructure.pandautils.utils.getFragmentActivity import com.instructure.pandautils.utils.remove import com.instructure.pandautils.utils.requestPermissions import com.instructure.student.R -import com.instructure.student.features.documentscanning.DocumentScanningActivity import com.instructure.student.mobius.assignmentDetails.isIntentAvailable import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode.CommentAttachment import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode.FileSubmission @@ -117,9 +115,6 @@ class PickerSubmissionUploadEffectHandler( PickerSubmissionUploadEffect.LaunchSelectFile -> { launchSelectFile() } - PickerSubmissionUploadEffect.LaunchDocumentScanning -> { - launchDocumentScanning() - } is PickerSubmissionUploadEffect.LoadFileContents -> { loadFile(effect.allowedExtensions, effect.uri, context) } @@ -210,17 +205,6 @@ class PickerSubmissionUploadEffectHandler( } } - private fun launchDocumentScanning() { - // Get camera permission if we need it - if (needsPermissions( - PickerSubmissionUploadEvent.DocumentScanningClicked, - PermissionUtils.CAMERA - ) - ) return - val intent = Intent(context, DocumentScanningActivity::class.java) - (context.getFragmentActivity()).startActivityForResult(intent, REQUEST_DOCUMENT_SCANNING) - } - private fun launchCamera() { // Get camera permission if we need it if (needsPermissions( diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt index 0a2ca09bfa..20c7544991 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt @@ -25,7 +25,6 @@ sealed class PickerSubmissionUploadEvent { object CameraClicked : PickerSubmissionUploadEvent() object GalleryClicked : PickerSubmissionUploadEvent() object SelectFileClicked : PickerSubmissionUploadEvent() - object DocumentScanningClicked : PickerSubmissionUploadEvent() data class OnFileSelected(val uri: Uri) : PickerSubmissionUploadEvent() data class OnFileRemoved(val fileIndex: Int) : PickerSubmissionUploadEvent() data class OnFileAdded(val file: FileSubmitObject?) : PickerSubmissionUploadEvent() @@ -35,7 +34,6 @@ sealed class PickerSubmissionUploadEffect { object LaunchCamera : PickerSubmissionUploadEffect() object LaunchGallery : PickerSubmissionUploadEffect() object LaunchSelectFile : PickerSubmissionUploadEffect() - object LaunchDocumentScanning : PickerSubmissionUploadEffect() data class HandleSubmit(val model: PickerSubmissionUploadModel) : PickerSubmissionUploadEffect() data class LoadFileContents(val uri: Uri, val allowedExtensions: List) : PickerSubmissionUploadEffect() diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt index 00b68bcedb..c16a600954 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt @@ -38,7 +38,6 @@ class PickerSubmissionUploadUpdate : PickerSubmissionUploadEvent.CameraClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.LaunchCamera)) PickerSubmissionUploadEvent.GalleryClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.LaunchGallery)) PickerSubmissionUploadEvent.SelectFileClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.LaunchSelectFile)) - PickerSubmissionUploadEvent.DocumentScanningClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.LaunchDocumentScanning)) is PickerSubmissionUploadEvent.OnFileSelected -> { Next.next( model.copy(isLoadingFile = true), diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt index 7a6d08e6ce..8a5f4cfe74 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt @@ -60,7 +60,6 @@ class PickerSubmissionUploadView(inflater: LayoutInflater, parent: ViewGroup, va binding.sourceDevice.setOnClickListener { consumer?.accept(PickerSubmissionUploadEvent.SelectFileClicked) } binding.sourceGallery.setOnClickListener { consumer?.accept(PickerSubmissionUploadEvent.GalleryClicked) } - binding.sourceDocumentScanning.setOnClickListener { consumer?.accept(PickerSubmissionUploadEvent.DocumentScanningClicked) } } override fun onConnect(output: Consumer) { diff --git a/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt index 8010db1157..ab26515598 100644 --- a/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt +++ b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt @@ -21,7 +21,6 @@ import android.webkit.WebView import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.ContextCompat import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.instructure.pandautils.utils.filecache.FileCache import com.instructure.canvasapi2.utils.Analytics import com.instructure.canvasapi2.utils.AnalyticsEventConstants import com.instructure.canvasapi2.utils.Logger @@ -32,14 +31,13 @@ import com.instructure.pandautils.utils.AppTheme import com.instructure.pandautils.utils.AppType import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.filecache.FileCache import com.instructure.student.BuildConfig import com.instructure.student.R import com.instructure.student.activity.NavigationActivity -import com.pspdfkit.PSPDFKit -import com.pspdfkit.exceptions.InvalidPSPDFKitLicenseException -import com.pspdfkit.exceptions.PSPDFKitInitializationFailedException -import com.pspdfkit.initialization.InitializationOptions -import com.zynksoftware.documentscanner.ui.DocumentScanner +import com.pspdfkit.Nutrient +import com.pspdfkit.exceptions.InvalidNutrientLicenseException +import com.pspdfkit.exceptions.NutrientInitializationFailedException abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), AnalyticsEventHandling { @@ -61,9 +59,7 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), Analyti // Hold off on initializing this until we emit the user properties. RemoteConfigUtils.initialize() - initPSPDFKit() - - initDocumentScanning() + initNutrient() if (BuildConfig.DEBUG) { FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false) @@ -110,19 +106,15 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), Analyti } - private fun initPSPDFKit() { + private fun initNutrient() { try { - PSPDFKit.initialize(this, InitializationOptions(licenseKey = BuildConfig.PSPDFKIT_LICENSE_KEY)) - } catch (e: PSPDFKitInitializationFailedException) { - Logger.e("Current device is not compatible with PSPDFKIT!") - } catch (e: InvalidPSPDFKitLicenseException) { - Logger.e("Invalid or Trial PSPDFKIT License!") + Nutrient.initialize(this, BuildConfig.PSPDFKIT_LICENSE_KEY) + } catch (e: NutrientInitializationFailedException) { + Logger.e("Current device is not compatible with Nutrient!") + } catch (e: InvalidNutrientLicenseException) { + Logger.e("Invalid or Trial Nutrient License!") } } - private fun initDocumentScanning() { - DocumentScanner.init(this) - } - override fun performLogoutOnAuthError() = Unit } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt b/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt index 73fb0b1066..1e34d0599f 100644 --- a/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt +++ b/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt @@ -72,20 +72,14 @@ object FileUtils { // We don't want to allow users to edit for submission viewing pspdfActivityConfiguration = PdfActivityConfiguration.Builder(context) .scrollDirection(PageScrollDirection.HORIZONTAL) - .showThumbnailGrid() .setThumbnailBarMode(ThumbnailBarMode.THUMBNAIL_BAR_MODE_PINNED) - .disableAnnotationEditing() - .disableAnnotationList() - .disableDocumentEditor() .fitMode(PageFitMode.FIT_TO_WIDTH) .build() } else { // Standard behavior pspdfActivityConfiguration = PdfActivityConfiguration.Builder(context) .scrollDirection(PageScrollDirection.HORIZONTAL) - .showThumbnailGrid() .setDocumentInfoViewSeparated(false) - .enableDocumentEditor() .enabledAnnotationTools(annotationCreationList) .editableAnnotationTypes(annotationEditList) .fitMode(PageFitMode.FIT_TO_WIDTH) diff --git a/apps/student/src/main/res/layout/activity_document_scanning.xml b/apps/student/src/main/res/layout/activity_document_scanning.xml deleted file mode 100644 index a8b7347a77..0000000000 --- a/apps/student/src/main/res/layout/activity_document_scanning.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/student/src/main/res/layout/activity_navigation.xml b/apps/student/src/main/res/layout/activity_navigation.xml index d508f631e4..cdbdefd661 100644 --- a/apps/student/src/main/res/layout/activity_navigation.xml +++ b/apps/student/src/main/res/layout/activity_navigation.xml @@ -61,6 +61,7 @@ android:id="@+id/bottomBar" android:layout_width="match_parent" android:layout_height="wrap_content" + android:minHeight="48dp" android:background="@color/backgroundLightestElevated" app:elevation="0dp" app:itemIconTint="@color/textDarkest" diff --git a/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml b/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml index 96be4e68e1..73b14a4553 100644 --- a/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml +++ b/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml @@ -160,36 +160,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle index 94405a5a01..29113262da 100644 --- a/apps/teacher/build.gradle +++ b/apps/teacher/build.gradle @@ -16,7 +16,8 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-kapt' // Keep kapt for Data Binding +apply plugin: 'com.google.devtools.ksp' apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'org.jetbrains.kotlin.plugin.compose' @@ -32,6 +33,25 @@ android { exclude 'META-INF/rxjava.properties' } + packaging { + resources { + pickFirsts += [ + 'META-INF/INDEX.LIST', + 'META-INF/io.netty.versions.properties' + ] + excludes += [ + 'META-INF/DEPENDENCIES', + 'META-INF/LICENSE', + 'META-INF/LICENSE.txt', + 'META-INF/NOTICE', + 'META-INF/NOTICE.txt', + 'META-INF/maven/com.google.guava/guava/pom.properties', + 'META-INF/maven/com.google.guava/guava/pom.xml', + 'META-INF/rxjava.properties' + ] + } + } + defaultConfig { minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK @@ -262,6 +282,9 @@ dependencies { implementation Libs.ANDROIDX_VECTOR implementation Libs.PLAY_IN_APP_UPDATES + /* Analytics */ + implementation Libs.PENDO + /* Firebase */ implementation platform(Libs.FIREBASE_BOM) { exclude group: 'com.google.firebase', module: 'firebase-analytics' @@ -271,36 +294,28 @@ dependencies { transitive = true } - testImplementation Libs.ANDROIDX_CORE_TESTING + implementation Libs.CAMERA_VIEW - /* AAC */ - implementation Libs.VIEW_MODEL - implementation Libs.LIVE_DATA - implementation Libs.VIEW_MODE_SAVED_STATE - implementation Libs.ANDROIDX_FRAGMENT_KTX - kapt Libs.LIFECYCLE_COMPILER + testImplementation Libs.ANDROIDX_CORE_TESTING /* DI */ implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER androidTestImplementation Libs.HILT_TESTING - kaptAndroidTestQa Libs.HILT_TESTING_COMPILER + kspAndroidTest Libs.HILT_TESTING_COMPILER implementation Libs.HILT_ANDROIDX_WORK - kapt Libs.HILT_ANDROIDX_COMPILER - - androidTestImplementation Libs.UI_AUTOMATOR - - /* WorkManager */ - implementation Libs.ANDROIDX_WORK_MANAGER - implementation Libs.ANDROIDX_WORK_MANAGER_KTX + ksp Libs.HILT_ANDROIDX_COMPILER - implementation Libs.PENDO - - implementation Libs.CAMERA_VIEW + /* AAC */ + implementation Libs.VIEW_MODEL + implementation Libs.LIVE_DATA + implementation Libs.VIEW_MODE_SAVED_STATE + implementation Libs.ANDROIDX_FRAGMENT_KTX + kapt Libs.LIFECYCLE_COMPILER // Keep kapt for lifecycle if it includes Data Binding - /* ROOM */ + /* Room */ implementation Libs.ROOM - kapt Libs.ROOM_COMPILER + ksp Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES testImplementation Libs.HAMCREST diff --git a/apps/teacher/proguard-rules.txt b/apps/teacher/proguard-rules.txt index 0157f11519..41f4c46430 100644 --- a/apps/teacher/proguard-rules.txt +++ b/apps/teacher/proguard-rules.txt @@ -258,4 +258,19 @@ -dontwarn java.beans.SimpleBeanInfo -keep class androidx.navigation.** { *; } - -keep interface androidx.navigation.** { *; } \ No newline at end of file + -keep interface androidx.navigation.** { *; } + +# Netty and BlockHound integration +-dontwarn reactor.blockhound.integration.BlockHoundIntegration +-dontwarn io.netty.util.internal.Hidden$NettyBlockHoundIntegration +-keep class reactor.blockhound.integration.** { *; } +-keep class io.netty.util.internal.Hidden$NettyBlockHoundIntegration { *; } + +# Additional Netty keep rules for R8 +-dontwarn io.netty.** +-keep class io.netty.** { *; } +-keepclassmembers class io.netty.** { *; } + +# BlockHound related classes +-dontwarn reactor.blockhound.** +-keep class reactor.blockhound.** { *; } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TestAppManager.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TestAppManager.kt index fcc640a20e..fb528a505e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TestAppManager.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TestAppManager.kt @@ -16,6 +16,7 @@ */ package com.instructure.teacher.espresso +import androidx.work.DefaultWorkerFactory import androidx.work.WorkerFactory import com.instructure.pandautils.features.reminder.AlarmScheduler import com.instructure.teacher.utils.BaseAppManager @@ -25,7 +26,7 @@ open class TestAppManager : BaseAppManager() { var workerFactory: WorkerFactory? = null override fun getWorkManagerFactory(): WorkerFactory { - return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() + return workerFactory ?: DefaultWorkerFactory } override fun getScheduler(): AlarmScheduler? = null diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt index 45e402a879..366460115e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt @@ -16,7 +16,9 @@ package com.instructure.teacher.ui.pages.compose import androidx.annotation.StringRes +import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextContains import androidx.compose.ui.test.hasAnyAncestor @@ -24,7 +26,7 @@ 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.onChildren +import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -66,7 +68,6 @@ import com.instructure.espresso.swipeToTop import com.instructure.teacher.R import org.hamcrest.Matchers import org.hamcrest.Matchers.allOf -import org.junit.Assert.assertEquals import java.util.Locale /** @@ -252,15 +253,17 @@ class SpeedGraderPage(private val composeTestRule: ComposeTestRule) : BasePage() */ fun selectCommentLibraryResultItem(index: Int? = null) { - val textNodes = composeTestRule - .onNodeWithTag("commentLibraryListColumn") - .onChildren() - .fetchSemanticsNodes() - - val targetText = textNodes[index ?: 0] - .config[androidx.compose.ui.semantics.SemanticsProperties.Text] + val targetText = composeTestRule + .onAllNodesWithTag("commentLibraryItem")[index ?: 0] + .fetchSemanticsNode() + .config[SemanticsProperties.Text] .joinToString("") { it.text } - composeTestRule.onNode(hasText(targetText, substring = true) and !(hasTestTag("ownCommentText"))) + composeTestRule.onNode( + hasText( + targetText, + substring = true + ) and (hasTestTag("ownCommentText").not()) + ) .performScrollTo() .performClick() composeTestRule.waitForIdle() @@ -290,12 +293,9 @@ class SpeedGraderPage(private val composeTestRule: ComposeTestRule) : BasePage() @OptIn(ExperimentalTestApi::class) fun assertCommentLibraryItemCount(expectedCount: Int) { composeTestRule.waitUntilExactlyOneExists(hasTestTagThatContains("commentLibraryListColumn"), timeoutMillis = 5000) - val textNodes = composeTestRule - .onNodeWithTag("commentLibraryListColumn") - .onChildren() - .fetchSemanticsNodes() - - assertEquals(expectedCount, textNodes.size) + composeTestRule + .onAllNodesWithTag("commentLibraryItem") + .assertCountEquals(expectedCount) } /** @@ -359,7 +359,7 @@ class SpeedGraderPage(private val composeTestRule: ComposeTestRule) : BasePage() getStringFromResource( R.string.sg_tab_files_w_counter, fileCount - ).toUpperCase() + ).uppercase() ) filesTab.click() } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/PSPDFKit/AnnotationComments/AnnotationCommentViewHolder.kt b/apps/teacher/src/main/java/com/instructure/teacher/PSPDFKit/AnnotationComments/AnnotationCommentViewHolder.kt index 03c4108c24..c66fcf72b2 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/PSPDFKit/AnnotationComments/AnnotationCommentViewHolder.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/PSPDFKit/AnnotationComments/AnnotationCommentViewHolder.kt @@ -51,7 +51,7 @@ class AnnotationCommentViewHolder(private val binding: AdapterAnnotationCommentB } commentEditIcon.onClick { - val popup = PopupMenu(context, it, Gravity.TOP, 0, com.google.android.material.R.style.Widget_AppCompat_PopupMenu_Overflow) + val popup = PopupMenu(context, it, Gravity.TOP, 0, com.google.android.material.R.style.Widget_Material3_PopupMenu_Overflow) popup.inflate(R.menu.menu_edit_annotation_comment) if(!canEdit) popup.menu.removeItem(R.id.edit) if(!canDelete) popup.menu.removeItem(R.id.delete) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt index eb150acd28..2ecbcaf3df 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt @@ -345,9 +345,9 @@ class FileListFragment : BaseSyncFragment< }) } - override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { + override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { workInfoLiveData.observe(viewLifecycleOwner) { - if (it.state == WorkInfo.State.SUCCEEDED) { + if (it?.state == WorkInfo.State.SUCCEEDED) { presenter.refresh(true) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderCommentsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderCommentsFragment.kt index 22fc3b92de..563dddce6c 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderCommentsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderCommentsFragment.kt @@ -303,8 +303,9 @@ class SpeedGraderCommentsFragment : BaseListFragment) { + override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { workInfoLiveData.observe(this) { + if (it == null) return@observe presenter.onFileUploadWorkInfoChanged(it) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt b/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt index 15296bc223..ebbdb3e8ac 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt @@ -21,7 +21,6 @@ import android.os.Build import androidx.appcompat.app.AppCompatDelegate import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.instructure.pandautils.utils.filecache.FileCache import com.instructure.canvasapi2.utils.AnalyticsEventConstants import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.Logger @@ -36,14 +35,14 @@ import com.instructure.pandautils.utils.AppType import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.filecache.FileCache import com.instructure.teacher.BuildConfig import com.instructure.teacher.R import com.instructure.teacher.activities.InitActivity import com.instructure.teacher.tasks.TeacherLogoutTask -import com.pspdfkit.PSPDFKit -import com.pspdfkit.exceptions.InvalidPSPDFKitLicenseException -import com.pspdfkit.exceptions.PSPDFKitInitializationFailedException -import com.pspdfkit.initialization.InitializationOptions +import com.pspdfkit.Nutrient +import com.pspdfkit.exceptions.InvalidNutrientLicenseException +import com.pspdfkit.exceptions.NutrientInitializationFailedException abstract class BaseAppManager : com.instructure.canvasapi2.AppManager() { @@ -76,11 +75,11 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager() { ColorKeeper.defaultColor = getColorCompat(R.color.textDarkest) try { - PSPDFKit.initialize(this, InitializationOptions(licenseKey = BuildConfig.PSPDFKIT_LICENSE_KEY)) - } catch (e: PSPDFKitInitializationFailedException) { - Logger.e("Current device is not compatible with PSPDFKIT!") - } catch (e: InvalidPSPDFKitLicenseException) { - Logger.e("Invalid or Trial PSPDFKIT License!") + Nutrient.initialize(this, BuildConfig.PSPDFKIT_LICENSE_KEY) + } catch (e: NutrientInitializationFailedException) { + Logger.e("Current device is not compatible with Nutrient!") + } catch (e: InvalidNutrientLicenseException) { + Logger.e("Invalid or Trial Nutrient License!") } MasqueradeHelper.masqueradeLogoutTask = Runnable { diff --git a/apps/teacher/src/main/res/layout/activity_init.xml b/apps/teacher/src/main/res/layout/activity_init.xml index 699d4456ba..46c52d9b83 100644 --- a/apps/teacher/src/main/res/layout/activity_init.xml +++ b/apps/teacher/src/main/res/layout/activity_init.xml @@ -112,6 +112,7 @@ android:id="@+id/bottomBar" android:layout_width="match_parent" android:layout_height="56dp" + android:minHeight="48dp" android:layout_alignParentBottom="true" android:background="@color/backgroundLightestElevated" app:itemIconTint="@color/textDarkest" diff --git a/automation/espresso/build.gradle b/automation/espresso/build.gradle index 8a48e8502a..4b7874ce96 100644 --- a/automation/espresso/build.gradle +++ b/automation/espresso/build.gradle @@ -37,7 +37,7 @@ allprojects { apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' android { @@ -175,9 +175,9 @@ dependencies { /* DI */ implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER implementation Libs.HILT_TESTING - kapt Libs.HILT_TESTING_COMPILER + ksp Libs.HILT_TESTING_COMPILER implementation Libs.HILT_ANDROIDX_WORK implementation Libs.COMPOSE_UI_TEST_MANIFEST diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt index 9e77a41f1f..f2e876daad 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt @@ -14,6 +14,7 @@ * limitations under the License. */package com.instructure.canvas.espresso +import androidx.work.DefaultWorkerFactory import android.annotation.SuppressLint import android.content.Context import android.util.Log @@ -27,22 +28,23 @@ import com.instructure.canvasapi2.utils.RemoteConfigUtils open class TestAppManager: AppManager() { + var testDriver: TestDriver? = null + + var workerFactory: WorkerFactory? = null + @SuppressLint("RestrictedApi") override fun onCreate() { super.onCreate() RemoteConfigUtils.initialize() if (workerFactory == null) { - workerFactory = WorkerFactory.getDefaultWorkerFactory() + workerFactory = getWorkManagerFactory() } } - var testDriver: TestDriver? = null - - var workerFactory: WorkerFactory? = null @SuppressLint("RestrictedApi") override fun getWorkManagerFactory(): WorkerFactory { - return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() + return workerFactory ?: DefaultWorkerFactory } override fun performLogoutOnAuthError() = Unit diff --git a/automation/soseedygrpc/build.gradle b/automation/soseedygrpc/build.gradle index d83c2030fb..ba6cdfef85 100644 --- a/automation/soseedygrpc/build.gradle +++ b/automation/soseedygrpc/build.gradle @@ -23,7 +23,7 @@ dependencies { compile project(':dataseedingapi') // https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22io.grpc%22%20a%3A%22grpc-netty%22 - compile 'io.grpc:grpc-netty:1.61.1' + compile 'io.grpc:grpc-netty:1.75.0' compile Libs.KOTLIN_STD_LIB // https://mvnrepository.com/artifact/io.netty/netty-tcnative-boringssl-static diff --git a/gradle/gradle/wrapper/gradle-wrapper.properties b/gradle/gradle/wrapper/gradle-wrapper.properties index b82aa23a4f..37f853b1c8 100644 --- a/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/libs/DocumentScanner/build.gradle b/libs/DocumentScanner/build.gradle deleted file mode 100644 index 98764308cc..0000000000 --- a/libs/DocumentScanner/build.gradle +++ /dev/null @@ -1,91 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -group="com.zynkware" - -def libraryVersionCode = 5 -def libraryVersionName = "1.0.1" - -repositories { - mavenCentral() - google() - maven { url "https://jitpack.io" } -} - -android { - namespace 'com.zynksoftware.documentscanner' - compileSdkVersion Versions.COMPILE_SDK - buildToolsVersion Versions.BUILD_TOOLS - - defaultConfig { - minSdkVersion 21 - targetSdkVersion Versions.TARGET_SDK - versionCode libraryVersionCode - versionName libraryVersionName - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17 - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - main.res.srcDirs = ['src/main/res'] - main.manifest.srcFile 'src/main/AndroidManifest.xml' - } - - buildFeatures { - viewBinding true - } -} - -repositories { - mavenCentral() - google() - maven { url 'https://jitpack.io' } -} - -dependencies { - implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation Libs.KOTLIN_STD_LIB - - implementation 'androidx.core:core-ktx:1.12.0' - implementation Libs.ANDROIDX_APPCOMPAT - - implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' - - implementation 'com.github.zynkware:Tiny-OpenCV:4.4.0-3' - - implementation "androidx.camera:camera-camera2:1.4.2" - implementation "androidx.camera:camera-lifecycle:1.4.2" - implementation "androidx.camera:camera-view:1.4.2" - - implementation 'androidx.exifinterface:exifinterface:1.4.0' - implementation Libs.KOTLIN_COROUTINES_ANDROID - implementation 'id.zelory:compressor:3.0.1' -} - -task sourceJar(type: Jar) { - from android.sourceSets.main.java.srcDirs - from fileTree(dir: 'src/libs', include: ['*.jar']) -} - -task androidSourcesJar(type: Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/AndroidManifest.xml b/libs/DocumentScanner/src/main/AndroidManifest.xml deleted file mode 100644 index 679e8fc75a..0000000000 --- a/libs/DocumentScanner/src/main/AndroidManifest.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ScanActivity.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ScanActivity.kt deleted file mode 100644 index e388c63ca0..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ScanActivity.kt +++ /dev/null @@ -1,29 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner - -import com.zynksoftware.documentscanner.ui.scan.InternalScanActivity - -abstract class ScanActivity : InternalScanActivity() { - - fun addFragmentContentLayout() { - addFragmentContentLayoutInternal() - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/BitmapExtensions.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/BitmapExtensions.kt deleted file mode 100644 index 8afd1e1f98..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/BitmapExtensions.kt +++ /dev/null @@ -1,51 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.extensions - -import android.graphics.Bitmap -import android.graphics.Matrix -import android.graphics.RectF -import org.opencv.android.Utils -import org.opencv.core.CvType -import org.opencv.core.Mat -import org.opencv.core.Scalar - -internal fun Bitmap.rotateBitmap(angle: Int): Bitmap { - val matrix = Matrix() - matrix.postRotate(angle.toFloat()) - return Bitmap.createBitmap(this, 0, 0, this.width, this.height, matrix, true) -} - -internal fun Bitmap.toMat(): Mat { - val mat = Mat(this.height, this.width, CvType.CV_8U, Scalar(4.toDouble())) - val bitmap32 = this.copy(Bitmap.Config.ARGB_8888, true) - Utils.bitmapToMat(bitmap32, mat) - return mat -} - -internal fun Bitmap.scaledBitmap(width: Int, height: Int): Bitmap { - val m = Matrix() - m.setRectToRect( - RectF(0f, 0f, this.width.toFloat(), this.height.toFloat()), - RectF(0f, 0f, width.toFloat(), height.toFloat()), - Matrix.ScaleToFit.CENTER - ) - return Bitmap.createBitmap(this, 0, 0, this.width, this.height, m, true) -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ImageProxyExtensions.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ImageProxyExtensions.kt deleted file mode 100644 index f0b6716c1f..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ImageProxyExtensions.kt +++ /dev/null @@ -1,93 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.extensions - -import android.graphics.ImageFormat -import androidx.camera.core.ImageProxy -import org.opencv.core.CvType -import org.opencv.core.Mat -import org.opencv.imgproc.Imgproc - -internal fun ImageProxy.yuvToRgba(): Mat { - val rgbaMat = Mat() - - if (format == ImageFormat.YUV_420_888 - && planes.size == 3) { - - val chromaPixelStride = planes[1].pixelStride - - if (chromaPixelStride == 2) { // Chroma channels are interleaved - val yPlane = planes[0].buffer - val uvPlane1 = planes[1].buffer - val uvPlane2 = planes[2].buffer - - val yMat = Mat((height), width, CvType.CV_8UC1, yPlane) - val uvMat1 = Mat(height / 2, width / 2, CvType.CV_8UC2, uvPlane1) - val uvMat2 = Mat(height / 2, width / 2, CvType.CV_8UC2, uvPlane2) - val addrDiff = uvMat2.dataAddr() - uvMat1.dataAddr() - if (addrDiff > 0) { - Imgproc.cvtColorTwoPlane(yMat, uvMat1, rgbaMat, Imgproc.COLOR_YUV2RGBA_NV12) - } else { - Imgproc.cvtColorTwoPlane(yMat, uvMat2, rgbaMat, Imgproc.COLOR_YUV2RGBA_NV21) - } - } else { // Chroma channels are not interleaved - val yuvBytes = ByteArray(width * (height + height / 2)) - val yPlane = planes[0].buffer - val uPlane = planes[1].buffer - val vPlane = planes[2].buffer - - yPlane.get(yuvBytes, 0, width * height) - - val chromaRowStride = planes[1].rowStride - val chromaRowPadding = chromaRowStride - width / 2 - - var offset = width * height - if (chromaRowPadding == 0) { - // When the row stride of the chroma channels equals their width, we can copy - // the entire channels in one go - uPlane.get(yuvBytes, offset, width * height / 4) - offset += width * height / 4 - vPlane.get(yuvBytes, offset, width * height / 4) - } else { - // When not equal, we need to copy the channels row by row - for (i in 0 until height / 2) { - uPlane.get(yuvBytes, offset, width / 2) - offset += width / 2 - if (i < height / 2 - 1) { - uPlane.position(uPlane.position() + chromaRowPadding) - } - } - for (i in 0 until height / 2) { - vPlane.get(yuvBytes, offset, width / 2) - offset += width / 2 - if (i < height / 2 - 1) { - vPlane.position(vPlane.position() + chromaRowPadding) - } - } - } - - val yuvMat = Mat(height + height / 2, width, CvType.CV_8UC1) - yuvMat.put(0, 0, yuvBytes) - Imgproc.cvtColor(yuvMat, rgbaMat, Imgproc.COLOR_YUV2BGR_NV21, 4) - } - } - - return rgbaMat -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/OpenCvExtensions.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/OpenCvExtensions.kt deleted file mode 100644 index 4d6e7f4779..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/OpenCvExtensions.kt +++ /dev/null @@ -1,42 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.extensions - -import android.graphics.Bitmap -import org.opencv.android.Utils -import org.opencv.core.* -import java.util.* - -internal fun Mat.toBitmap(): Bitmap { - val bitmap = Bitmap.createBitmap(this.cols(), this.rows(), Bitmap.Config.ARGB_8888) - Utils.matToBitmap(this, bitmap) - return bitmap -} - -internal fun MatOfPoint2f.scaleRectangle(scale: Double): MatOfPoint2f { - val originalPoints = this.toList() - val resultPoints: MutableList = ArrayList() - for (point in originalPoints) { - resultPoints.add(Point(point.x * scale, point.y * scale)) - } - val result = MatOfPoint2f() - result.fromList(resultPoints) - return result -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ViewExtensions.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ViewExtensions.kt deleted file mode 100644 index 17375b9edb..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ViewExtensions.kt +++ /dev/null @@ -1,30 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.extensions - -import android.view.View - -internal fun View.hide() { - visibility = View.GONE -} - -internal fun View.show() { - visibility = View.VISIBLE -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/FileUriUtils.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/FileUriUtils.kt deleted file mode 100644 index f65058f84b..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/FileUriUtils.kt +++ /dev/null @@ -1,240 +0,0 @@ -package com.zynksoftware.documentscanner.common.utils - -import android.content.ContentUris -import android.content.Context -import android.database.Cursor -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.DocumentsContract -import android.provider.MediaStore -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream - -/** - * This file was taken from - * https://gist.github.com/HBiSoft/15899990b8cd0723c3a894c1636550a8 - * - * Later on it was modified from the below resource: - * https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java - * https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java - */ - -internal object FileUriUtils { - - fun getRealPath(context: Context, uri: Uri): String? { - var path = getPathFromLocalUri(context, uri) - if (path == null) { - path = getPathFromRemoteUri(context, uri) - } - return path - } - - private fun getPathFromLocalUri(context: Context, uri: Uri): String? { - - val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT - - // DocumentProvider - if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { - // ExternalStorageProvider - if (isExternalStorageDocument(uri)) { - val docId = DocumentsContract.getDocumentId(uri) - val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - val type = split[0] - - // This is for checking Main Memory - return if ("primary".equals(type, ignoreCase = true)) { - if (split.size > 1) { - Environment.getExternalStorageDirectory().toString() + "/" + split[1] - } else { - Environment.getExternalStorageDirectory().toString() + "/" - } - // This is for checking SD Card - } else { - val path = "storage" + "/" + docId.replace(":", "/") - if (File(path).exists()) { - path - } else { - "/storage/sdcard/" + split[1] - } - } - } else if (isDownloadsDocument(uri)) { - val fileName = getFilePath(context, uri) - if (fileName != null) { - return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName - } - - val id = DocumentsContract.getDocumentId(uri) - val contentUri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id) - ) - return getDataColumn(context, contentUri, null, null) - } else if (isMediaDocument(uri)) { - val docId = DocumentsContract.getDocumentId(uri) - val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - val type = split[0] - - var contentUri: Uri? = null - if ("image" == type) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI - } else if ("video" == type) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI - } else if ("audio" == type) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - } - - val selection = "_id=?" - val selectionArgs = arrayOf(split[1]) - - return getDataColumn(context, contentUri, selection, selectionArgs) - } // MediaProvider - // DownloadsProvider - } else if ("content".equals(uri.scheme!!, ignoreCase = true)) { - - // Return the remote address - return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(context, uri, null, null) - } else if ("file".equals(uri.scheme!!, ignoreCase = true)) { - return uri.path - } // File - // MediaStore (and general) - - return null - } - - private fun getDataColumn( - context: Context, - uri: Uri?, - selection: String?, - selectionArgs: Array? - ): String? { - - var cursor: Cursor? = null - val column = "_data" - val projection = arrayOf(column) - - try { - cursor = context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) - if (cursor != null && cursor.moveToFirst()) { - val index = cursor.getColumnIndexOrThrow(column) - return cursor.getString(index) - } - } catch (ex: Exception) { - } finally { - cursor?.close() - } - return null - } - - private fun getFilePath(context: Context, uri: Uri): String? { - - var cursor: Cursor? = null - val projection = arrayOf(MediaStore.MediaColumns.DISPLAY_NAME) - - try { - cursor = context.contentResolver.query(uri, projection, null, null, null) - if (cursor != null && cursor.moveToFirst()) { - val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) - return cursor.getString(index) - } - } finally { - cursor?.close() - } - return null - } - - private fun getPathFromRemoteUri(context: Context, uri: Uri): String? { - // The code below is why Java now has try-with-resources and the Files utility. - var file: File? = null - var inputStream: InputStream? = null - var outputStream: OutputStream? = null - var success = false - try { - val extension = getImageExtension(uri) - inputStream = context.contentResolver.openInputStream(uri) - val storageDir = context.cacheDir - if (!storageDir.exists()) { - storageDir.mkdirs() - } - file = File(storageDir, "remotePicture${extension}") - file.createNewFile() - outputStream = FileOutputStream(file) - if (inputStream != null) { - inputStream.copyTo(outputStream, bufferSize = 4 * 1024) - success = true - } - } catch (ignored: IOException) { - } finally { - try { - inputStream?.close() - } catch (ignored: IOException) { - } - - try { - outputStream?.close() - } catch (ignored: IOException) { - // If closing the output stream fails, we cannot be sure that the - // target file was written in full. Flushing the stream merely moves - // the bytes into the OS, not necessarily to the file. - success = false - } - } - return if (success) file!!.path else null - } - - /** @return extension of image with dot, or default .jpg if it none. - */ - private fun getImageExtension(uriImage: Uri): String { - var extension: String? = null - - try { - val imagePath = uriImage.path - if (imagePath != null && imagePath.lastIndexOf(".") != -1) { - extension = imagePath.substring(imagePath.lastIndexOf(".") + 1) - } - } catch (e: Exception) { - extension = null - } - - if (extension == null || extension.isEmpty()) { - // default extension for matches the previous behavior of the plugin - extension = "jpg" - } - - return ".$extension" - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is ExternalStorageProvider. - */ - private fun isExternalStorageDocument(uri: Uri): Boolean { - return "com.android.externalstorage.documents" == uri.authority - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is DownloadsProvider. - */ - private fun isDownloadsDocument(uri: Uri): Boolean { - return "com.android.providers.downloads.documents" == uri.authority - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is MediaProvider. - */ - private fun isMediaDocument(uri: Uri): Boolean { - return "com.android.providers.media.documents" == uri.authority - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is Google Photos. - */ - private fun isGooglePhotosUri(uri: Uri): Boolean { - return "com.google.android.apps.photos.content" == uri.authority - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/ImageDetectionProperties.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/ImageDetectionProperties.kt deleted file mode 100644 index 39310e1df5..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/ImageDetectionProperties.kt +++ /dev/null @@ -1,81 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.utils - -import org.opencv.core.MatOfPoint2f -import org.opencv.core.Point -import kotlin.math.abs - -internal class ImageDetectionProperties( - private val previewWidth: Double, private val previewHeight: Double, - private val topLeftPoint: Point, private val bottomLeftPoint: Point, - private val bottomRightPoint: Point, private val topRightPoint: Point, - private val resultWidth: Int, private val resultHeight: Int -) { - - companion object { - private const val SMALLEST_ANGLE_COS = 0.172 //80 degrees - } - - fun isNotValidImage(approx: MatOfPoint2f): Boolean { - return isEdgeTouching || isAngleNotCorrect(approx) || isDetectedAreaBelowLimits() - } - - private fun isAngleNotCorrect(approx: MatOfPoint2f): Boolean { - return getMaxCosine(approx) || isLeftEdgeDistorted || isRightEdgeDistorted - } - - private val isRightEdgeDistorted: Boolean - get() = abs(topRightPoint.y - bottomRightPoint.y) > 100 - - private val isLeftEdgeDistorted: Boolean - get() = abs(topLeftPoint.y - bottomLeftPoint.y) > 100 - - private fun getMaxCosine(approx: MatOfPoint2f): Boolean { - var maxCosine = 0.0 - val approxPoints = approx.toArray() - maxCosine = MathUtils.getMaxCosine(maxCosine, approxPoints) - return maxCosine >= SMALLEST_ANGLE_COS - } - - private val isEdgeTouching: Boolean - get() = isTopEdgeTouching || isBottomEdgeTouching || isLeftEdgeTouching || isRightEdgeTouching - - private val isBottomEdgeTouching: Boolean - get() = bottomLeftPoint.x >= previewHeight - 10 || bottomRightPoint.x >= previewHeight - 10 - - private val isTopEdgeTouching: Boolean - get() = topLeftPoint.x <= 10 || topRightPoint.x <= 10 - - private val isRightEdgeTouching: Boolean - get() = topRightPoint.y >= previewWidth - 10 || bottomRightPoint.y >= previewWidth - 10 - - private val isLeftEdgeTouching: Boolean - get() = topLeftPoint.y <= 10 || bottomLeftPoint.y <= 10 - - private fun isDetectedAreaBelowLimits(): Boolean { - return !(previewWidth / previewHeight >= 1 && - resultWidth.toDouble() / resultHeight.toDouble() >= 0.9 && - resultHeight.toDouble() >= 0.70 * previewHeight || - previewHeight / previewWidth >= 1 && - resultHeight.toDouble() / resultWidth.toDouble() >= 0.9 && - resultWidth.toDouble() >= 0.70 * previewWidth) - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/MathUtils.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/MathUtils.kt deleted file mode 100644 index 1c6b1d16d0..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/MathUtils.kt +++ /dev/null @@ -1,51 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.utils - -import org.opencv.core.Point -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.sqrt - -internal object MathUtils { - - private fun angle(p1: Point, p2: Point, p0: Point): Double { - val dx1 = p1.x - p0.x - val dy1 = p1.y - p0.y - val dx2 = p2.x - p0.x - val dy2 = p2.y - p0.y - return (dx1 * dx2 + dy1 * dy2) / sqrt((dx1 * dx1 + dy1 * dy1) * (dx2 * dx2 + dy2 * dy2) + 1e-10) - } - - fun getDistance(p1: Point, p2: Point): Double { - val dx = p2.x - p1.x - val dy = p2.y - p1.y - return sqrt(dx * dx + dy * dy) - } - - fun getMaxCosine(maxCosine: Double, approxPoints: Array): Double { - var newMaxCosine = maxCosine - for (i in 2..4) { - val cosine: Double = abs(angle(approxPoints[i % 4], approxPoints[i - 2], approxPoints[i - 1])) - newMaxCosine = max(cosine, newMaxCosine) - } - return newMaxCosine - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/OpenCvNativeBridge.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/OpenCvNativeBridge.kt deleted file mode 100644 index 0916409902..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/OpenCvNativeBridge.kt +++ /dev/null @@ -1,247 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.utils - -import android.graphics.Bitmap -import android.graphics.PointF -import com.zynksoftware.documentscanner.common.extensions.scaleRectangle -import com.zynksoftware.documentscanner.common.extensions.toBitmap -import com.zynksoftware.documentscanner.common.extensions.toMat -import com.zynksoftware.documentscanner.ui.components.Quadrilateral -import org.opencv.core.* -import org.opencv.imgproc.Imgproc -import java.util.* -import kotlin.collections.ArrayList -import kotlin.math.* - - -internal class OpenCvNativeBridge { - - companion object { - private const val ANGLES_NUMBER = 4 - private const val EPSILON_CONSTANT = 0.02 - private const val CLOSE_KERNEL_SIZE = 10.0 - private const val CANNY_THRESHOLD_LOW = 75.0 - private const val CANNY_THRESHOLD_HIGH = 200.0 - private const val CUTOFF_THRESHOLD = 155.0 - private const val TRUNCATE_THRESHOLD = 150.0 - private const val NORMALIZATION_MIN_VALUE = 0.0 - private const val NORMALIZATION_MAX_VALUE = 255.0 - private const val BLURRING_KERNEL_SIZE = 5.0 - private const val DOWNSCALE_IMAGE_SIZE = 600.0 - private const val FIRST_MAX_CONTOURS = 10 - } - - fun getScannedBitmap(bitmap: Bitmap, x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float, x4: Float, y4: Float): Bitmap { - val rectangle = MatOfPoint2f() - rectangle.fromArray( - Point(x1.toDouble(), y1.toDouble()), - Point(x2.toDouble(), y2.toDouble()), - Point(x3.toDouble(), y3.toDouble()), - Point(x4.toDouble(), y4.toDouble()) - ) - val dstMat = PerspectiveTransformation.transform(bitmap.toMat(), rectangle) - return dstMat.toBitmap() - } - - fun getContourEdgePoints(tempBitmap: Bitmap): List { - var point2f = getPoint(tempBitmap) - if (point2f == null) point2f = MatOfPoint2f() - val points: List = point2f.toArray().toList() - val result: MutableList = ArrayList() - for (i in points.indices) { - result.add(PointF(points[i].x.toFloat(), points[i].y.toFloat())) - } - - return result - } - - fun getPoint(bitmap: Bitmap): MatOfPoint2f? { - val src = bitmap.toMat() - - val ratio = DOWNSCALE_IMAGE_SIZE / max(src.width(), src.height()) - val downscaledSize = Size(src.width() * ratio, src.height() * ratio) - val downscaled = Mat(downscaledSize, src.type()) - Imgproc.resize(src, downscaled, downscaledSize) - val largestRectangle = detectLargestQuadrilateral(downscaled) - - return largestRectangle?.contour?.scaleRectangle(1f / ratio) - } - - // patch from Udayraj123 (https://github.com/Udayraj123/LiveEdgeDetection) - fun detectLargestQuadrilateral(src: Mat): Quadrilateral? { - val destination = Mat() - Imgproc.blur(src, src, Size(BLURRING_KERNEL_SIZE, BLURRING_KERNEL_SIZE)) - - Core.normalize(src, src, NORMALIZATION_MIN_VALUE, NORMALIZATION_MAX_VALUE, Core.NORM_MINMAX) - - Imgproc.threshold(src, src, TRUNCATE_THRESHOLD, NORMALIZATION_MAX_VALUE, Imgproc.THRESH_TRUNC) - Core.normalize(src, src, NORMALIZATION_MIN_VALUE, NORMALIZATION_MAX_VALUE, Core.NORM_MINMAX) - - Imgproc.Canny(src, destination, CANNY_THRESHOLD_HIGH, CANNY_THRESHOLD_LOW) - - Imgproc.threshold(destination, destination, CUTOFF_THRESHOLD, NORMALIZATION_MAX_VALUE, Imgproc.THRESH_TOZERO) - - Imgproc.morphologyEx( - destination, destination, Imgproc.MORPH_CLOSE, - Mat(Size(CLOSE_KERNEL_SIZE, CLOSE_KERNEL_SIZE), CvType.CV_8UC1, Scalar(NORMALIZATION_MAX_VALUE)), - Point(-1.0, -1.0), 1 - ) - - val largestContour: List? = findLargestContours(destination) - if (null != largestContour) { - return findQuadrilateral(largestContour) - } - return null - } - - private fun findQuadrilateral(mContourList: List): Quadrilateral? { - for (c in mContourList) { - val c2f = MatOfPoint2f(*c.toArray()) - val peri = Imgproc.arcLength(c2f, true) - val approx = MatOfPoint2f() - Imgproc.approxPolyDP(c2f, approx, EPSILON_CONSTANT * peri, true) - val points = approx.toArray() - // select biggest 4 angles polygon - if (approx.rows() == ANGLES_NUMBER) { - val foundPoints: Array = sortPoints(points) - return Quadrilateral(approx, foundPoints) - } else if(approx.rows() == 5) { - // if document has a bent corner - var shortestDistance = Int.MAX_VALUE.toDouble() - var shortestPoint1: Point? = null - var shortestPoint2: Point? = null - - var diagonal = 0.toDouble() - var diagonalPoint1: Point? = null - var diagonalPoint2: Point? = null - - for (i in 0 until 4) { - for (j in i + 1 until 5) { - val d = distance(points[i], points[j]) - if (d < shortestDistance) { - shortestDistance = d - shortestPoint1 = points[i] - shortestPoint2 = points[j] - } - if(d > diagonal) { - diagonal = d - diagonalPoint1 = points[i] - diagonalPoint2 = points[j] - } - } - } - - val trianglePointWithHypotenuse: Point? = points.toList().minus(arrayListOf(shortestPoint1, shortestPoint2, diagonalPoint1, diagonalPoint2))[0] - - val newPoint = if(trianglePointWithHypotenuse!!.x > shortestPoint1!!.x && trianglePointWithHypotenuse.x > shortestPoint2!!.x && - trianglePointWithHypotenuse.y > shortestPoint1.y && trianglePointWithHypotenuse.y > shortestPoint2.y) { - Point(min(shortestPoint1.x, shortestPoint2.x), min(shortestPoint1.y, shortestPoint2.y)) - } else if(trianglePointWithHypotenuse.x < shortestPoint1.x && trianglePointWithHypotenuse.x < shortestPoint2!!.x && - trianglePointWithHypotenuse.y > shortestPoint1.y && trianglePointWithHypotenuse.y > shortestPoint2.y) { - Point(max(shortestPoint1.x, shortestPoint2.x), min(shortestPoint1.y, shortestPoint2.y)) - } else if(trianglePointWithHypotenuse.x < shortestPoint1.x && trianglePointWithHypotenuse.x < shortestPoint2!!.x && - trianglePointWithHypotenuse.y < shortestPoint1.y && trianglePointWithHypotenuse.y < shortestPoint2.y) { - Point(max(shortestPoint1.x, shortestPoint2.x), max(shortestPoint1.y, shortestPoint2.y)) - } else if(trianglePointWithHypotenuse.x > shortestPoint1.x && trianglePointWithHypotenuse.x > shortestPoint2!!.x && - trianglePointWithHypotenuse.y < shortestPoint1.y && trianglePointWithHypotenuse.y < shortestPoint2.y) { - Point(min(shortestPoint1.x, shortestPoint2.x), max(shortestPoint1.y, shortestPoint2.y)) - } else { - Point(0.0, 0.0) - } - - val sortedPoints = sortPoints(arrayOf(trianglePointWithHypotenuse, diagonalPoint1!!, diagonalPoint2!!, newPoint)) - val newApprox = MatOfPoint2f() - newApprox.fromArray(*sortedPoints) - return Quadrilateral(newApprox, sortedPoints) - } - } - return null - } - - private fun distance(p1: Point, p2: Point): Double { - return sqrt((p1.x - p2.x).pow(2.0) + (p1.y - p2.y).pow(2.0)) - } - - private fun sortPoints(src: Array): Array { - val srcPoints: ArrayList = ArrayList(src.toList()) - val result = arrayOf(null, null, null, null) - val sumComparator: Comparator = Comparator { lhs, rhs -> (lhs.y + lhs.x).compareTo(rhs.y + rhs.x) } - val diffComparator: Comparator = Comparator { lhs, rhs -> (lhs.y - lhs.x).compareTo(rhs.y - rhs.x) } - - // top-left corner = minimal sum - result[0] = Collections.min(srcPoints, sumComparator) - // bottom-right corner = maximal sum - result[2] = Collections.max(srcPoints, sumComparator) - // top-right corner = minimal difference - result[1] = Collections.min(srcPoints, diffComparator) - // bottom-left corner = maximal difference - result[3] = Collections.max(srcPoints, diffComparator) - return result.map { - it!! - }.toTypedArray() - } - - private fun findLargestContours(inputMat: Mat): List? { - val mHierarchy = Mat() - val mContourList: List = ArrayList() - //finding contours - as we are sorting by area anyway, we can use RETR_LIST - faster than RETR_EXTERNAL. - Imgproc.findContours(inputMat, mContourList, mHierarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE) - - // Convert the contours to their Convex Hulls i.e. removes minor nuances in the contour - val mHullList: MutableList = ArrayList() - val tempHullIndices = MatOfInt() - for (i in mContourList.indices) { - Imgproc.convexHull(mContourList[i], tempHullIndices) - mHullList.add(hull2Points(tempHullIndices, mContourList[i])) - } - // Release mContourList as its job is done - for (c in mContourList) { - c.release() - } - tempHullIndices.release() - mHierarchy.release() - if (mHullList.size != 0) { - mHullList.sortWith { lhs, rhs -> - Imgproc.contourArea(rhs).compareTo(Imgproc.contourArea(lhs)) - } - return mHullList.subList(0, min(mHullList.size, FIRST_MAX_CONTOURS)) - } - return null - } - - private fun hull2Points(hull: MatOfInt, contour: MatOfPoint): MatOfPoint { - val indexes = hull.toList() - val points: MutableList = ArrayList() - val ctrList = contour.toList() - for (index in indexes) { - points.add(ctrList[index]) - } - val point = MatOfPoint() - point.fromList(points) - return point - } - - fun contourArea(approx: MatOfPoint2f): Double { - return Imgproc.contourArea(approx) - } - - -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/PerspectiveTransformation.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/PerspectiveTransformation.kt deleted file mode 100644 index d5a077b8f9..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/PerspectiveTransformation.kt +++ /dev/null @@ -1,117 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.utils - -import com.zynksoftware.documentscanner.common.utils.MathUtils.getDistance -import org.opencv.core.Mat -import org.opencv.core.MatOfPoint2f -import org.opencv.core.Point -import org.opencv.core.Size -import org.opencv.imgproc.Imgproc -import java.util.* - -internal object PerspectiveTransformation { - - fun transform(src: Mat, corners: MatOfPoint2f): Mat { - val sortedCorners = sortCorners(corners) - val size = getRectangleSize(sortedCorners) - val result = Mat.zeros(size, src.type()) - val imageOutline = getOutline(result) - val transformation = Imgproc.getPerspectiveTransform(sortedCorners, imageOutline) - Imgproc.warpPerspective(src, result, transformation, size) - return result - } - - private fun getRectangleSize(rectangle: MatOfPoint2f): Size { - val corners = rectangle.toArray() - val top = getDistance(corners[0], corners[1]) - val right = getDistance(corners[1], corners[2]) - val bottom = getDistance(corners[2], corners[3]) - val left = getDistance(corners[3], corners[0]) - val averageWidth = (top + bottom) / 2f - val averageHeight = (right + left) / 2f - return Size(Point(averageWidth, averageHeight)) - } - - private fun getOutline(image: Mat): MatOfPoint2f { - val topLeft = Point(0.toDouble(), 0.toDouble()) - val topRight = Point(image.cols().toDouble(), 0.toDouble()) - val bottomRight = Point(image.cols().toDouble(), image.rows().toDouble()) - val bottomLeft = Point(0.toDouble(), image.rows().toDouble()) - val points = arrayOf(topLeft, topRight, bottomRight, bottomLeft) - val result = MatOfPoint2f() - result.fromArray(*points) - return result - } - - private fun sortCorners(corners: MatOfPoint2f): MatOfPoint2f { - val center = getMassCenter(corners) - val points = corners.toList() - val topPoints: MutableList = ArrayList() - val bottomPoints: MutableList = ArrayList() - for (point in points) { - if (point.y < center.y) { - topPoints.add(point) - } else { - bottomPoints.add(point) - } - } - - val topLeft = if (topPoints[0].x > topPoints[1].x) { - topPoints[1] - } else { - topPoints[0] - } - - val topRight = if (topPoints[0].x > topPoints[1].x) { - topPoints[0] - } else { - topPoints[1] - } - - val bottomLeft = if (bottomPoints[0].x > bottomPoints[1].x) { - bottomPoints[1] - } else { - bottomPoints[0] - } - - val bottomRight = if (bottomPoints[0].x > bottomPoints[1].x) { - bottomPoints[0] - } else { - bottomPoints[1] - } - val result = MatOfPoint2f() - val sortedPoints = arrayOf(topLeft, topRight, bottomRight, bottomLeft) - result.fromArray(*sortedPoints) - return result - } - - private fun getMassCenter(points: MatOfPoint2f): Point { - var xSum = 0.0 - var ySum = 0.0 - val pointList = points.toList() - val len = pointList.size - for (point in pointList) { - xSum += point.x - ySum += point.y - } - return Point(xSum / len, ySum / len) - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/manager/SessionManager.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/manager/SessionManager.kt deleted file mode 100644 index b1d786476a..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/manager/SessionManager.kt +++ /dev/null @@ -1,68 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.manager - -import android.content.Context -import android.graphics.Bitmap -import id.zelory.compressor.extension -import java.util.Locale - -internal class SessionManager(context: Context) { - - companion object { - private const val IMAGE_SIZE_KEY = "IMAGE_SIZE_KEY" - private const val IMAGE_QUALITY_KEY = "IMAGE_QUALITY_KEY" - private const val IMAGE_TYPE_KEY = "IMAGE_TYPE_KEY" - - private const val DEFAULT_IMAGE_TYPE = "jpg" - } - private val preferences = context.getSharedPreferences("ZDC_Shared_Preferences", Context.MODE_PRIVATE) - - - fun getImageSize(): Long { - return preferences.getLong(IMAGE_SIZE_KEY, -1L) - } - - fun setImageSize(size: Long) { - preferences.edit().putLong(IMAGE_SIZE_KEY, size).apply() - } - - fun getImageQuality(): Int { - return preferences.getInt(IMAGE_QUALITY_KEY, 100) - } - - fun setImageQuality(quality: Int) { - preferences.edit().putInt(IMAGE_QUALITY_KEY, quality).apply() - } - - fun getImageType(): Bitmap.CompressFormat { - return compressFormat(preferences.getString(IMAGE_TYPE_KEY, DEFAULT_IMAGE_TYPE)!!) - } - - fun setImageType(type: Bitmap.CompressFormat) { - preferences.edit().putString(IMAGE_TYPE_KEY, type.extension()).apply() - } - - private fun compressFormat(format: String) = when (format.lowercase(Locale.getDefault())) { - "png" -> Bitmap.CompressFormat.PNG - "webp" -> Bitmap.CompressFormat.WEBP - else -> Bitmap.CompressFormat.JPEG - } -} diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/DocumentScannerErrorModel.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/DocumentScannerErrorModel.kt deleted file mode 100644 index 1cc0f9d99a..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/DocumentScannerErrorModel.kt +++ /dev/null @@ -1,38 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.model - -data class DocumentScannerErrorModel( - var errorMessage: ErrorMessage? = null, - var throwable: Throwable? = null -) { - enum class ErrorMessage(val error: String){ - TAKE_IMAGE_FROM_GALLERY_ERROR("TAKE_IMAGE_FROM_GALLERY_ERROR"), - PHOTO_CAPTURE_FAILED("PHOTO_CAPTURE_FAILED"), - CAMERA_USE_CASE_BINDING_FAILED("CAMERA_USE_CASE_BINDING_FAILED"), - DETECT_LARGEST_QUADRILATERAL_FAILED("DETECT_LARGEST_QUADRILATERAL_FAILED"), - INVALID_IMAGE("INVALID_IMAGE"), - CAMERA_PERMISSION_REFUSED_WITHOUT_NEVER_ASK_AGAIN("CAMERA_PERMISSION_REFUSED_WITHOUT_NEVER_ASK_AGAIN"), - CAMERA_PERMISSION_REFUSED_GO_TO_SETTINGS("CAMERA_PERMISSION_REFUSED_GO_TO_SETTINGS"), - STORAGE_PERMISSION_REFUSED_WITHOUT_NEVER_ASK_AGAIN("STORAGE_PERMISSION_REFUSED_WITHOUT_NEVER_ASK_AGAIN"), - STORAGE_PERMISSION_REFUSED_GO_TO_SETTINGS("STORAGE_PERMISSION_REFUSED_GO_TO_SETTINGS"), - CROPPING_FAILED("CROPPING_FAILED"); - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/ScannerResults.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/ScannerResults.kt deleted file mode 100644 index 6bdf126742..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/ScannerResults.kt +++ /dev/null @@ -1,28 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.model - -import java.io.File - -data class ScannerResults ( - val originalImageFile: File? = null, - val croppedImageFile: File? = null, - val transformedImageFile: File? = null -) \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/DocumentScanner.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/DocumentScanner.kt deleted file mode 100644 index c335a5093c..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/DocumentScanner.kt +++ /dev/null @@ -1,46 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui - -import android.content.Context -import android.graphics.Bitmap -import com.zynksoftware.documentscanner.manager.SessionManager - -object DocumentScanner { - - fun init(context: Context, configuration: Configuration = Configuration()) { - System.loadLibrary("opencv_java4") - val sessionManager = SessionManager(context) - if(configuration.imageQuality in 1..100) { - sessionManager.setImageQuality(configuration.imageQuality) - } - sessionManager.setImageSize(configuration.imageSize) - sessionManager.setImageType(configuration.imageType) - } - - - data class Configuration( - var imageQuality: Int = 100, - var imageSize: Long = -1, - var imageType: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG - ){ - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/base/BaseFragment.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/base/BaseFragment.kt deleted file mode 100644 index 0984bb5311..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/base/BaseFragment.kt +++ /dev/null @@ -1,58 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.ui.base - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.RelativeLayout -import androidx.fragment.app.Fragment -import androidx.viewbinding.ViewBinding -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.common.extensions.hide -import com.zynksoftware.documentscanner.common.extensions.show - -internal abstract class BaseFragment(private val bindingInflater: (layoutInflater: LayoutInflater) -> BINDING) : Fragment() { - - private var _binding: BINDING? = null - - // This property is only valid between onCreateView and onDestroyView. - val binding get() = _binding!! - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - _binding = bindingInflater(inflater) - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - fun showProgressBar() { - view?.findViewById(R.id.progressLayout)?.show() - } - - fun hideProgressBar() { - view?.findViewById(R.id.progressLayout)?.hide() - } - -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/camerascreen/CameraScreenFragment.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/camerascreen/CameraScreenFragment.kt deleted file mode 100644 index 903a80640c..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/camerascreen/CameraScreenFragment.kt +++ /dev/null @@ -1,213 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.ui.camerascreen - -import android.Manifest -import android.net.Uri -import android.os.Bundle -import android.util.Log -import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.common.extensions.hide -import com.zynksoftware.documentscanner.common.extensions.show -import com.zynksoftware.documentscanner.common.utils.FileUriUtils -import com.zynksoftware.documentscanner.databinding.FragmentCameraScreenBinding -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.ui.base.BaseFragment -import com.zynksoftware.documentscanner.ui.components.scansurface.ScanSurfaceListener -import com.zynksoftware.documentscanner.ui.scan.InternalScanActivity -import java.io.File -import java.io.FileNotFoundException - - -internal class CameraScreenFragment: BaseFragment(FragmentCameraScreenBinding::inflate), ScanSurfaceListener { - - private val requestCameraPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - if (isGranted) { - startCamera() - } else { - onError(DocumentScannerErrorModel(DocumentScannerErrorModel.ErrorMessage.CAMERA_PERMISSION_REFUSED_GO_TO_SETTINGS)) - } - } - - private val filePickerContract = registerForActivityResult(ActivityResultContracts.GetContent()) { - handleSelectedFile(it) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) { - super.onViewCreated(view, savedInstanceState) - - scanSurfaceView.lifecycleOwner = this@CameraScreenFragment - scanSurfaceView.listener = this@CameraScreenFragment - scanSurfaceView.originalImageFile = getScanActivity().originalImageFile - - checkForCameraPermissions() - initListeners() - } - - override fun onDestroy() { - super.onDestroy() - if(getScanActivity().shouldCallOnClose) { - getScanActivity().onClose() - } - } - - override fun onResume() { - super.onResume() - getScanActivity().reInitOriginalImageFile() - binding.scanSurfaceView.originalImageFile = getScanActivity().originalImageFile - } - - private fun initListeners() = with(binding) { - cameraCaptureButton.setOnClickListener { - takePhoto() - } - cancelButton.setOnClickListener { - finishActivity() - } - flashButton.setOnClickListener { - switchFlashState() - } - galleryButton.setOnClickListener { - selectImageFromGallery() - } - autoButton.setOnClickListener { - toggleAutoManualButton() - } - } - - private fun toggleAutoManualButton() = with(binding) { - scanSurfaceView.isAutoCaptureOn = !scanSurfaceView.isAutoCaptureOn - if (scanSurfaceView.isAutoCaptureOn) { - autoButton.text = getString(R.string.zdc_auto) - } else { - autoButton.text = getString(R.string.zdc_manual) - } - } - - private fun checkForCameraPermissions() { - ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.CAMERA).let { - if (it == android.content.pm.PackageManager.PERMISSION_GRANTED) { - startCamera() - } else { - requestCameraPermission.launch(Manifest.permission.CAMERA) - } - } - } - - private fun startCamera() { - binding.scanSurfaceView.start() - } - - private fun takePhoto() { - binding.scanSurfaceView.takePicture() - } - - private fun getScanActivity(): InternalScanActivity { - return (requireActivity() as InternalScanActivity) - } - - private fun finishActivity() { - getScanActivity().finish() - } - - private fun switchFlashState() { - binding.scanSurfaceView.switchFlashState() - } - - override fun showFlash() { - binding.flashButton.show() - } - - override fun hideFlash() { - binding.flashButton.hide() - } - - private fun selectImageFromGallery() { - filePickerContract.launch("image/*") - } - - private fun handleSelectedFile(imageUri: Uri?) { - try { - if (imageUri != null) { - val realPath = FileUriUtils.getRealPath(getScanActivity(), imageUri) - if (realPath != null) { - getScanActivity().reInitOriginalImageFile() - getScanActivity().originalImageFile = File(realPath) - startCroppingProcess() - } else { - Log.e(TAG, DocumentScannerErrorModel.ErrorMessage.TAKE_IMAGE_FROM_GALLERY_ERROR.error) - onError(DocumentScannerErrorModel( - DocumentScannerErrorModel.ErrorMessage.TAKE_IMAGE_FROM_GALLERY_ERROR, null)) - } - } else { - Log.e(TAG, DocumentScannerErrorModel.ErrorMessage.TAKE_IMAGE_FROM_GALLERY_ERROR.error) - onError(DocumentScannerErrorModel( - DocumentScannerErrorModel.ErrorMessage.TAKE_IMAGE_FROM_GALLERY_ERROR, null)) - } - } catch (e: FileNotFoundException) { - Log.e(TAG, "FileNotFoundException", e) - onError(DocumentScannerErrorModel( - DocumentScannerErrorModel.ErrorMessage.TAKE_IMAGE_FROM_GALLERY_ERROR, e)) - } - } - - override fun scanSurfacePictureTaken() { - startCroppingProcess() - } - - private fun startCroppingProcess() { - if (isAdded) { - getScanActivity().showImageCropFragment() - } - } - - override fun scanSurfaceShowProgress() { - showProgressBar() - } - - override fun scanSurfaceHideProgress() { - hideProgressBar() - } - - override fun onError(error: DocumentScannerErrorModel) { - if(isAdded) { - getScanActivity().onError(error) - } - } - - override fun showFlashModeOn() { - binding.flashButton.setImageResource(R.drawable.zdc_flash_on) - } - - override fun showFlashModeOff() { - binding.flashButton.setImageResource(R.drawable.zdc_flash_off) - } - - companion object { - private val TAG = CameraScreenFragment::class.simpleName - - fun newInstance(): CameraScreenFragment { - return CameraScreenFragment() - } - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ProgressView.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ProgressView.kt deleted file mode 100644 index f8ab5787e8..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ProgressView.kt +++ /dev/null @@ -1,38 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.RelativeLayout -import com.zynksoftware.documentscanner.R - -internal class ProgressView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : RelativeLayout(context, attrs, defStyleAttr) { - - init { - LayoutInflater.from(context).inflate(R.layout.progress_layout, this, true) - } -} diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/Quadrilateral.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/Quadrilateral.kt deleted file mode 100644 index e00482089e..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/Quadrilateral.kt +++ /dev/null @@ -1,26 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components - -import org.opencv.core.MatOfPoint2f -import org.opencv.core.Point - -internal class Quadrilateral(val contour: MatOfPoint2f, val points: Array) \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ScanCanvasView.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ScanCanvasView.kt deleted file mode 100644 index af550b984b..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ScanCanvasView.kt +++ /dev/null @@ -1,183 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Path -import android.os.Handler -import android.os.Looper -import android.util.AttributeSet -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.core.content.ContextCompat -import com.zynksoftware.documentscanner.R -import org.opencv.core.Point - -internal class ScanCanvasView : FrameLayout { - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) - - companion object { - private const val CLEAR_SHAPE_DELAY_IN_MILLIS = 600L - private const val POINTER_ANIMATION_DURATION = 300L - } - - private var paint = Paint() - private var border = Paint() - private val handlerClear = Handler(Looper.getMainLooper()) - - private var shouldAnimate = true - - var pointer1: View = View(context) - var pointer2: View = View(context) - var pointer3: View = View(context) - var pointer4: View = View(context) - - init { - paint.color = ContextCompat.getColor(context, R.color.zdc_white_transparent) - border.color = ContextCompat.getColor(context, android.R.color.white) - border.strokeWidth = context.resources.getDimension(R.dimen.zdc_polygon_line_stroke_width) - border.style = Paint.Style.STROKE - border.isAntiAlias = true - paint.isAntiAlias = true - - pointer1.layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - pointer2.layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - pointer3.layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - pointer4.layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - - clearPointersPosition() - - addView(pointer1) - addView(pointer2) - addView(pointer3) - addView(pointer4) - } - - private fun clearPointersPosition() { - pointer1.x = 0F - pointer1.y = 0F - pointer2.x = 0F - pointer2.y = 0F - pointer3.x = 0F - pointer3.y = 0F - pointer4.x = 0F - pointer4.y = 0F - } - - override fun dispatchDraw(canvas: Canvas) { - super.dispatchDraw(canvas) - - previewWidth?.let { previewWidth -> - previewHeight?.let { previewHeight -> - canvas.scale(width / previewWidth, height / previewHeight) - } - } - - canvas.drawLine(pointer1.x, pointer1.y, pointer4.x, pointer4.y, border) - canvas.drawLine(pointer1.x, pointer1.y, pointer2.x, pointer2.y, border) - canvas.drawLine(pointer3.x, pointer3.y, pointer4.x, pointer4.y, border) - canvas.drawLine(pointer2.x, pointer2.y, pointer3.x, pointer3.y, border) - - val path = Path() - path.moveTo(pointer1.x, pointer1.y) - path.lineTo(pointer2.x, pointer2.y) - path.lineTo(pointer3.x, pointer3.y) - path.lineTo(pointer4.x, pointer4.y) - path.close() - - path.let { - canvas.drawPath(it, paint) - } - } - - var previewWidth: Float? = null - var previewHeight: Float? = null - - fun showShape(previewWidth: Float, previewHeight: Float, points: Array) { - this.previewWidth = previewWidth - this.previewHeight = previewHeight - - val pointer1x = previewWidth - points[0].y.toFloat() - val pointer1y = points[0].x.toFloat() - val pointer2x = previewWidth - points[1].y.toFloat() - val pointer2y = points[1].x.toFloat() - val pointer3x = previewWidth - points[2].y.toFloat() - val pointer3y = points[2].x.toFloat() - val pointer4x = previewWidth - points[3].y.toFloat() - val pointer4y = points[3].x.toFloat() - - if (pointer1.x == 0F && pointer1.y == 0F) { - pointer1.x = pointer1x - pointer1.y = pointer1y - pointer2.x = pointer2x - pointer2.y = pointer2y - pointer3.x = pointer3x - pointer3.y = pointer3y - pointer4.x = pointer4x - pointer4.y = pointer4y - } else { - if (shouldAnimate) { - shouldAnimate = false - - pointer1.animate().translationX(pointer1x).translationY(pointer1y) - .setDuration(POINTER_ANIMATION_DURATION).withEndAction { - shouldAnimate = true - }.start() - - pointer2.animate().translationX(pointer2x).translationY(pointer2y) - .setDuration(POINTER_ANIMATION_DURATION).withEndAction { - shouldAnimate = true - }.start() - - pointer3.animate().translationX(pointer3x).translationY(pointer3y) - .setDuration(POINTER_ANIMATION_DURATION).withEndAction { - shouldAnimate = true - }.start() - - pointer4.animate().translationX(pointer4x).translationY(pointer4y) - .setDuration(POINTER_ANIMATION_DURATION).withEndAction { - shouldAnimate = true - }.start() - } - } - - handlerClear.removeCallbacks(runnable) - invalidate() - } - - fun clearShape() { - handlerClear.postDelayed(runnable, CLEAR_SHAPE_DELAY_IN_MILLIS) - } - - private val runnable = Runnable { - pointer1.clearAnimation() - pointer2.clearAnimation() - pointer3.clearAnimation() - pointer4.clearAnimation() - clearPointersPosition() - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonPointImageView.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonPointImageView.kt deleted file mode 100644 index 15b82ece1c..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonPointImageView.kt +++ /dev/null @@ -1,85 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.ui.components.polygon - -import android.content.Context -import android.graphics.PointF -import android.util.AttributeSet -import android.view.MotionEvent -import androidx.appcompat.widget.AppCompatImageView -import androidx.core.content.ContextCompat -import com.zynksoftware.documentscanner.R - -internal class PolygonPointImageView @JvmOverloads constructor( - context: Context, - private val polygonView: PolygonView? = null, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : AppCompatImageView(context, attrs, defStyleAttr) { - - private var downPoint = PointF() - private var startPoint = PointF() - - override fun onTouchEvent(event: MotionEvent): Boolean { - super.onTouchEvent(event) - - if (polygonView != null) { - when (event.action) { - MotionEvent.ACTION_MOVE -> { - val mv = PointF(event.x - downPoint.x, event.y - downPoint.y) - if (startPoint.x + mv.x + width < polygonView.width && - startPoint.y + mv.y + height < polygonView.height && - startPoint.x + mv.x > 0 && startPoint.y + mv.y > 0 - ) { - x = startPoint.x + mv.x - y = startPoint.y + mv.y - startPoint = PointF(x, y) - } - } - MotionEvent.ACTION_DOWN -> { - downPoint.x = event.x - downPoint.y = event.y - startPoint = PointF(x, y) - } - MotionEvent.ACTION_UP -> { - performClick() - } - } - polygonView.invalidate() - } - return true - } - - // Because we call this from onTouchEvent, this code will be executed for both - // normal touch events and for when the system calls this using Accessibility - override fun performClick(): Boolean { - super.performClick() - - val color = if (polygonView?.isValidShape(polygonView.getPoints()) == true) { - ContextCompat.getColor(context, android.R.color.white) - } else { - ContextCompat.getColor(context, R.color.zdc_red) - } - polygonView?.paint?.color = color - - return true - } - -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonView.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonView.kt deleted file mode 100644 index 001f697f5d..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonView.kt +++ /dev/null @@ -1,183 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components.polygon - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.PointF -import android.util.AttributeSet -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ImageView -import androidx.core.content.ContextCompat -import com.zynksoftware.documentscanner.R -import java.util.* - -internal class PolygonView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr) { - - var paint: Paint = Paint() - private var pointer1: ImageView - private var pointer2: ImageView - private var pointer3: ImageView - private var pointer4: ImageView - private var pointPadding = resources.getDimension(R.dimen.zdc_point_padding).toInt() - - companion object { - private val TAG = PolygonView::class.simpleName - private const val HALF = 2 - private const val THREE_PARTS = 3 - } - - init { - pointer1 = getImageView(0, 0) - pointer2 = getImageView(width, 0) - pointer3 = getImageView(0, height) - pointer4 = getImageView(width, height) - - addView(pointer1) - addView(pointer2) - addView(pointer3) - addView(pointer4) - - paint.color = ContextCompat.getColor(context, android.R.color.white) - paint.strokeWidth = context.resources.getDimension(R.dimen.zdc_polygon_line_stroke_width) - paint.isAntiAlias = true - } - - fun getOrderedValidEdgePoints(tempBitmap: Bitmap, pointFs: List): Map { - var orderedPoints: Map = getOrderedPoints(pointFs) - if (!isValidShape(orderedPoints)) { - orderedPoints = getOutlinePoints(tempBitmap) - } - return orderedPoints - } - - fun setPoints(pointFMap: Map) { - if (pointFMap.size == 4) { - setPointsCoordinates(pointFMap) - } - } - - fun getPoints(): Map { - val points: MutableList = ArrayList() - points.add(PointF(pointer1.x, pointer1.y)) - points.add(PointF(pointer2.x, pointer2.y)) - points.add(PointF(pointer3.x, pointer3.y)) - points.add(PointF(pointer4.x, pointer4.y)) - return getOrderedPoints(points) - } - - fun isValidShape(pointFMap: Map): Boolean { - return pointFMap.size == 4 - } - - private fun getOutlinePoints(tempBitmap: Bitmap): Map { - val offsetWidth = (tempBitmap.width / THREE_PARTS).toFloat() - val offsetHeight = (tempBitmap.height / THREE_PARTS).toFloat() - val screenXCenter = tempBitmap.width / HALF - val screenYCenter = tempBitmap.height / HALF - val outlinePoints: MutableMap = HashMap() - outlinePoints[0] = PointF(screenXCenter - offsetWidth, screenYCenter - offsetHeight) - outlinePoints[1] = PointF(screenXCenter + offsetWidth, screenYCenter - offsetHeight) - outlinePoints[2] = PointF(screenXCenter - offsetWidth , screenYCenter + offsetHeight) - outlinePoints[3] = PointF(screenXCenter + offsetWidth, screenYCenter + offsetHeight) - return outlinePoints - } - - private fun getOrderedPoints(points: List): Map { - val centerPoint = PointF() - val size = points.size - for (pointF in points) { - centerPoint.x += pointF.x / size - centerPoint.y += pointF.y / size - } - val orderedPoints: MutableMap = HashMap() - for (pointF in points) { - var index = -1 - if (pointF.x < centerPoint.x && pointF.y < centerPoint.y) { - index = 0 - } else if (pointF.x > centerPoint.x && pointF.y < centerPoint.y) { - index = 1 - } else if (pointF.x < centerPoint.x && pointF.y > centerPoint.y) { - index = 2 - } else if (pointF.x > centerPoint.x && pointF.y > centerPoint.y) { - index = 3 - } - orderedPoints[index] = pointF - } - return orderedPoints - } - - private fun setPointsCoordinates(pointFMap: Map) { - pointer1.x = pointFMap.getValue(0).x - pointPadding - pointer1.y = pointFMap.getValue(0).y - pointPadding - - pointer2.x = pointFMap.getValue(1).x - pointPadding - pointer2.y = pointFMap.getValue(1).y - pointPadding - - pointer3.x = pointFMap.getValue(2).x - pointPadding - pointer3.y = pointFMap.getValue(2).y - pointPadding - - pointer4.x = pointFMap.getValue(3).x - pointPadding - pointer4.y = pointFMap.getValue(3).y - pointPadding - } - - override fun dispatchDraw(canvas: Canvas) { - super.dispatchDraw(canvas) - canvas.drawLine( - pointer1.x + pointer1.width / 2, pointer1.y + pointer1.height / 2, - pointer3.x + pointer3.width / 2, pointer3.y + pointer3.height / 2, - paint - ) - canvas.drawLine( - pointer1.x + pointer1.width / 2, pointer1.y + pointer1.height / 2, - pointer2.x + pointer2.width / 2, pointer2.y + pointer2.height / 2, - paint - ) - canvas.drawLine( - pointer2.x + pointer2.width / 2, pointer2.y + pointer2.height / 2, - pointer4.x + pointer4.width / 2, pointer4.y + pointer4.height / 2, - paint - ) - canvas.drawLine( - pointer3.x + pointer3.width / 2, pointer3.y + pointer3.height / 2, - pointer4.x + pointer4.width / 2, pointer4.y + pointer4.height / 2, - paint - ) - } - - private fun getImageView(x: Int, y: Int): ImageView { - val imageView = PolygonPointImageView(context, this) - val layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - imageView.layoutParams = layoutParams - imageView.setImageResource(R.drawable.crop_corner_circle) - imageView.setPadding(pointPadding, pointPadding, pointPadding, pointPadding) - imageView.x = x.toFloat() - imageView.y = y.toFloat() - return imageView - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceListener.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceListener.kt deleted file mode 100644 index 0c742f817b..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceListener.kt +++ /dev/null @@ -1,35 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components.scansurface - -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel - -internal interface ScanSurfaceListener { - fun scanSurfacePictureTaken() - fun scanSurfaceShowProgress() - fun scanSurfaceHideProgress() - fun onError(error: DocumentScannerErrorModel) - - fun showFlash() - fun hideFlash() - fun showFlashModeOn() - fun showFlashModeOff() -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceView.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceView.kt deleted file mode 100755 index 88e14f42fb..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceView.kt +++ /dev/null @@ -1,289 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components.scansurface - -import android.content.Context -import android.os.CountDownTimer -import android.util.AttributeSet -import android.util.Log -import android.view.LayoutInflater -import android.view.Surface -import android.widget.FrameLayout -import androidx.camera.core.* -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.core.content.ContextCompat -import androidx.lifecycle.LifecycleOwner -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.common.extensions.yuvToRgba -import com.zynksoftware.documentscanner.common.utils.ImageDetectionProperties -import com.zynksoftware.documentscanner.common.utils.OpenCvNativeBridge -import com.zynksoftware.documentscanner.databinding.ScanSurfaceViewBinding -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel.ErrorMessage -import org.opencv.core.MatOfPoint2f -import org.opencv.core.Point -import org.opencv.core.Size -import java.io.File -import kotlin.math.max -import kotlin.math.min -import kotlin.math.roundToInt - -private val TAG = ScanSurfaceView::class.simpleName - -private const val TIME_POST_PICTURE = 1500L -private const val DEFAULT_TIME_POST_PICTURE = 1500L -private const val IMAGE_ANALYSIS_SCALE_WIDTH = 400 - -internal class ScanSurfaceView : FrameLayout { - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) - - private val binding: ScanSurfaceViewBinding - - lateinit var lifecycleOwner: LifecycleOwner - lateinit var listener: ScanSurfaceListener - lateinit var originalImageFile: File - - private val nativeClass = OpenCvNativeBridge() - private var autoCaptureTimer: CountDownTimer? = null - private var millisLeft = 0L - private var isAutoCaptureScheduled = false - private var isCapturing = false - - private var imageAnalysis: ImageAnalysis? = null - private var camera: Camera? = null - private var imageCapture: ImageCapture? = null - private var preview: Preview? = null - private var cameraProvider: ProcessCameraProvider? = null - private lateinit var previewSize: android.util.Size - - var isAutoCaptureOn: Boolean = true - private var isFlashEnabled: Boolean = false - private var flashMode: Int = ImageCapture.FLASH_MODE_OFF - - init { - binding = ScanSurfaceViewBinding.inflate(LayoutInflater.from(context), this, true) - } - - fun start() = with(binding) { - viewFinder.post { - viewFinder.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) - previewSize = android.util.Size(viewFinder.width, viewFinder.height) - openCamera() - } - } - - private fun clearAndInvalidateCanvas() { - binding.scanCanvasView.clearShape() - } - - private fun openCamera() { - val cameraProviderFuture = ProcessCameraProvider.getInstance(context) - - cameraProviderFuture.addListener(Runnable { - cameraProvider = cameraProviderFuture.get() - - try { - bindCamera() - checkIfFlashIsPresent() - } catch (exc: Exception) { - Log.e(TAG, ErrorMessage.CAMERA_USE_CASE_BINDING_FAILED.error, exc) - listener.onError(DocumentScannerErrorModel(ErrorMessage.CAMERA_USE_CASE_BINDING_FAILED, exc)) - } - }, ContextCompat.getMainExecutor(context)) - } - - private fun bindCamera() { - cameraProvider?.unbindAll() - camera = null - setUseCases() - } - - private fun setImageCapture() { - if(imageCapture != null && cameraProvider?.isBound(imageCapture!!) == true) { - cameraProvider?.unbind(imageCapture) - } - - imageCapture = null - imageCapture = ImageCapture.Builder() - .setTargetRotation(Surface.ROTATION_0) - .setFlashMode(flashMode) - .build() - } - - fun unbindCamera() { - cameraProvider?.unbind(imageAnalysis) - } - - private fun setUseCases() { - preview = Preview.Builder() - .setTargetResolution(previewSize) - .setTargetRotation(Surface.ROTATION_0) - .build() - .also { - it.setSurfaceProvider(binding.viewFinder.surfaceProvider) - } - - setImageCapture() - - val aspectRatio: Float = previewSize.width / previewSize.height.toFloat() - val width = IMAGE_ANALYSIS_SCALE_WIDTH - val height = (width / aspectRatio).roundToInt() - - imageAnalysis = ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .setTargetResolution(android.util.Size(width, height)) - .setTargetRotation(Surface.ROTATION_0) - .build() - - imageAnalysis?.setAnalyzer(ContextCompat.getMainExecutor(context), { image -> - if (isAutoCaptureOn) { - try { - val mat = image.yuvToRgba() - val originalPreviewSize = mat.size() - val largestQuad = nativeClass.detectLargestQuadrilateral(mat) - mat.release() - if (null != largestQuad) { - drawLargestRect(largestQuad.contour, largestQuad.points, originalPreviewSize) - } else { - clearAndInvalidateCanvas() - } - } catch (e: Exception) { - Log.e(TAG, ErrorMessage.DETECT_LARGEST_QUADRILATERAL_FAILED.error, e) - listener.onError(DocumentScannerErrorModel(ErrorMessage.DETECT_LARGEST_QUADRILATERAL_FAILED, e)) - clearAndInvalidateCanvas() - } - } else { - clearAndInvalidateCanvas() - } - image.close() - }) - - camera = cameraProvider!!.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis, imageCapture) - } - - private fun drawLargestRect(approx: MatOfPoint2f, points: Array, stdSize: Size) { - // Attention: axis are swapped - val previewWidth = stdSize.height.toFloat() - val previewHeight = stdSize.width.toFloat() - - val resultWidth = max(previewWidth - points[0].y.toFloat(), previewWidth - points[1].y.toFloat()) - - min(previewWidth - points[2].y.toFloat(), previewWidth - points[3].y.toFloat()) - - val resultHeight = max(points[1].x.toFloat(), points[2].x.toFloat()) - min(points[0].x.toFloat(), points[3].x.toFloat()) - - val imgDetectionPropsObj = ImageDetectionProperties(previewWidth.toDouble(), previewHeight.toDouble(), - points[0], points[1], points[2], points[3], resultWidth.toInt(), resultHeight.toInt()) - if (imgDetectionPropsObj.isNotValidImage(approx)) { - binding.scanCanvasView.clearShape() - cancelAutoCapture() - } else { - if (!isAutoCaptureScheduled) { - scheduleAutoCapture() - } - binding.scanCanvasView.showShape(previewWidth, previewHeight, points) - } - } - - private fun scheduleAutoCapture() { - isAutoCaptureScheduled = true - millisLeft = 0L - autoCaptureTimer = object : CountDownTimer(DEFAULT_TIME_POST_PICTURE, 100) { - override fun onTick(millisUntilFinished: Long) { - if (millisUntilFinished != millisLeft) { - millisLeft = millisUntilFinished - } - } - - override fun onFinish() { - isAutoCaptureScheduled = false - autoCapture() - } - } - autoCaptureTimer?.start() - } - - private fun autoCapture() { - if (isCapturing) - return - cancelAutoCapture() - takePicture() - } - - fun takePicture() { - Log.d(TAG, "ZDCtakePicture Starts ${System.currentTimeMillis()}") - listener.scanSurfaceShowProgress() - isCapturing = true - - val imageCapture = imageCapture ?: return - val outputOptions = ImageCapture.OutputFileOptions.Builder(originalImageFile).build() - - imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(context), - object : ImageCapture.OnImageSavedCallback { - override fun onError(exc: ImageCaptureException) { - listener.scanSurfaceHideProgress() - Log.e(TAG, "${ErrorMessage.PHOTO_CAPTURE_FAILED.error}: ${exc.message}", exc) - listener.onError(DocumentScannerErrorModel(ErrorMessage.PHOTO_CAPTURE_FAILED, exc)) - } - - override fun onImageSaved(output: ImageCapture.OutputFileResults) { - listener.scanSurfaceHideProgress() - - unbindCamera() - - clearAndInvalidateCanvas() - listener.scanSurfacePictureTaken() - postDelayed({ isCapturing = false }, TIME_POST_PICTURE) - Log.d(TAG, "ZDCtakePicture ends ${System.currentTimeMillis()}") - } - }) - } - - private fun checkIfFlashIsPresent() { - if (camera?.cameraInfo?.hasFlashUnit() == true) { - listener.showFlash() - } else { - listener.hideFlash() - } - } - - private fun cancelAutoCapture() { - if (isAutoCaptureScheduled) { - isAutoCaptureScheduled = false - autoCaptureTimer?.cancel() - } - } - - fun switchFlashState() { - isFlashEnabled = !isFlashEnabled - flashMode = if (isFlashEnabled) { - listener.showFlashModeOn() - ImageCapture.FLASH_MODE_ON - } else { - listener.showFlashModeOff() - ImageCapture.FLASH_MODE_OFF - } - setImageCapture() - camera = cameraProvider!!.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, imageCapture) - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imagecrop/ImageCropFragment.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imagecrop/ImageCropFragment.kt deleted file mode 100644 index 8ab1ef6633..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imagecrop/ImageCropFragment.kt +++ /dev/null @@ -1,159 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.imagecrop - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.PointF -import android.graphics.drawable.BitmapDrawable -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.Log -import android.view.Gravity -import android.view.View -import android.widget.FrameLayout -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.ScanActivity -import com.zynksoftware.documentscanner.common.extensions.scaledBitmap -import com.zynksoftware.documentscanner.common.utils.OpenCvNativeBridge -import com.zynksoftware.documentscanner.databinding.FragmentImageCropBinding -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.ui.base.BaseFragment -import com.zynksoftware.documentscanner.ui.scan.InternalScanActivity -import id.zelory.compressor.determineImageRotation - -internal class ImageCropFragment : BaseFragment(FragmentImageCropBinding::inflate) { - - private val nativeClass = OpenCvNativeBridge() - - private var selectedImage: Bitmap? = null - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val sourceBitmap = BitmapFactory.decodeFile(getScanActivity().originalImageFile.absolutePath) - if (sourceBitmap != null) { - selectedImage = determineImageRotation(getScanActivity().originalImageFile, sourceBitmap) - } else { - Log.e(TAG, DocumentScannerErrorModel.ErrorMessage.INVALID_IMAGE.error) - onError(DocumentScannerErrorModel(DocumentScannerErrorModel.ErrorMessage.INVALID_IMAGE)) - Handler(Looper.getMainLooper()).post{ - closeFragment() - } - } - binding.holderImageView.post { - if (this.view != null) initializeCropping() - } - - initListeners() - } - - private fun initListeners() = with(binding) { - closeButton.setOnClickListener { - closeFragment() - } - confirmButton.setOnClickListener { - onConfirmButtonClicked() - } - } - - private fun getScanActivity(): InternalScanActivity { - return (requireActivity() as InternalScanActivity) - } - - private fun initializeCropping() = with(binding) { - if(selectedImage != null && selectedImage!!.width > 0 && selectedImage!!.height > 0) { - val scaledBitmap: Bitmap = selectedImage!!.scaledBitmap(holderImageCrop.width, holderImageCrop.height) - imagePreview.setImageBitmap(scaledBitmap) - val tempBitmap = (imagePreview.drawable as BitmapDrawable).bitmap - val pointFs = getEdgePoints(tempBitmap) - Log.d(TAG, "ZDCgetEdgePoints ends ${System.currentTimeMillis()}") - polygonView.setPoints(pointFs) - polygonView.visibility = View.VISIBLE - val padding = resources.getDimension(R.dimen.zdc_polygon_dimens).toInt() - val layoutParams = FrameLayout.LayoutParams(tempBitmap.width + padding, tempBitmap.height + padding) - layoutParams.gravity = Gravity.CENTER - polygonView.layoutParams = layoutParams - } - } - - private fun onError(error: DocumentScannerErrorModel) { - if (isAdded) { - getScanActivity().onError(error) - } - } - - private fun onConfirmButtonClicked() { - getCroppedImage() - (activity as ScanActivity).finalScannerResult() - } - - private fun getEdgePoints(tempBitmap: Bitmap): Map { - Log.d(TAG, "ZDCgetEdgePoints Starts ${System.currentTimeMillis()}") - val pointFs: List = nativeClass.getContourEdgePoints(tempBitmap) - return binding.polygonView.getOrderedValidEdgePoints(tempBitmap, pointFs) - } - - private fun getCroppedImage() { - if(selectedImage != null) { - try { - Log.d(TAG, "ZDCgetCroppedImage starts ${System.currentTimeMillis()}") - val points: Map = binding.polygonView.getPoints() - val xRatio: Float = selectedImage!!.width.toFloat() / binding.imagePreview.width - val yRatio: Float = selectedImage!!.height.toFloat() / binding.imagePreview.height - val pointPadding = requireContext().resources.getDimension(R.dimen.zdc_point_padding).toInt() - val x1: Float = (points.getValue(0).x + pointPadding) * xRatio - val x2: Float = (points.getValue(1).x + pointPadding) * xRatio - val x3: Float = (points.getValue(2).x + pointPadding) * xRatio - val x4: Float = (points.getValue(3).x + pointPadding) * xRatio - val y1: Float = (points.getValue(0).y + pointPadding) * yRatio - val y2: Float = (points.getValue(1).y + pointPadding) * yRatio - val y3: Float = (points.getValue(2).y + pointPadding) * yRatio - val y4: Float = (points.getValue(3).y + pointPadding) * yRatio - getScanActivity().croppedImage = nativeClass.getScannedBitmap(selectedImage!!, x1, y1, x2, y2, x3, y3, x4, y4) - Log.d(TAG, "ZDCgetCroppedImage ends ${System.currentTimeMillis()}") - } catch (e: java.lang.Exception) { - Log.e(TAG, DocumentScannerErrorModel.ErrorMessage.CROPPING_FAILED.error, e) - onError(DocumentScannerErrorModel(DocumentScannerErrorModel.ErrorMessage.CROPPING_FAILED, e)) - } - } else { - Log.e(TAG, DocumentScannerErrorModel.ErrorMessage.INVALID_IMAGE.error) - onError(DocumentScannerErrorModel(DocumentScannerErrorModel.ErrorMessage.INVALID_IMAGE)) - } - } - - private fun startImageProcessingFragment() { - getScanActivity().showImageProcessingFragment() - } - - private fun closeFragment() { - getScanActivity().closeCurrentFragment() - } - - companion object { - private val TAG = ImageCropFragment::class.simpleName - - fun newInstance(): ImageCropFragment { - return ImageCropFragment() - } - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imageprocessing/ImageProcessingFragment.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imageprocessing/ImageProcessingFragment.kt deleted file mode 100644 index 735a8fb1d6..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imageprocessing/ImageProcessingFragment.kt +++ /dev/null @@ -1,136 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.imageprocessing - -import android.graphics.* -import android.os.Bundle -import android.util.Log -import android.view.View -import com.zynksoftware.documentscanner.common.extensions.rotateBitmap -import com.zynksoftware.documentscanner.databinding.FragmentImageProcessingBinding -import com.zynksoftware.documentscanner.ui.base.BaseFragment -import com.zynksoftware.documentscanner.ui.scan.InternalScanActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch - -internal class ImageProcessingFragment : BaseFragment(FragmentImageProcessingBinding::inflate) { - - private var isInverted = false - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.imagePreview.setImageBitmap(getScanActivity().croppedImage) - - initListeners() - } - - private fun initListeners() = with(binding) { - closeButton.setOnClickListener { - closeFragment() - } - confirmButton.setOnClickListener { - selectFinalScannerResults() - } - magicButton.setOnClickListener { - applyGrayScaleFilter() - } - rotateButton.setOnClickListener { - rotateImage() - } - } - - private fun getScanActivity(): InternalScanActivity { - return (requireActivity() as InternalScanActivity) - } - - private fun rotateImage() { - Log.d(TAG, "ZDCrotate starts ${System.currentTimeMillis()}") - showProgressBar() - GlobalScope.launch(Dispatchers.IO) { - if(isAdded) { - getScanActivity().transformedImage = getScanActivity().transformedImage?.rotateBitmap(ANGLE_OF_ROTATION) - getScanActivity().croppedImage = getScanActivity().croppedImage?.rotateBitmap(ANGLE_OF_ROTATION) - } - - if(isAdded) { - getScanActivity().runOnUiThread { - hideProgressBar() - if (isInverted) { - binding.imagePreview?.setImageBitmap(getScanActivity().transformedImage) - } else { - binding.imagePreview?.setImageBitmap(getScanActivity().croppedImage) - } - } - } - Log.d(TAG, "ZDCrotate ends ${System.currentTimeMillis()}") - } - } - - private fun closeFragment() { - getScanActivity().closeCurrentFragment() - } - - private fun applyGrayScaleFilter() { - Log.d(TAG, "ZDCgrayscale starts ${System.currentTimeMillis()}") - showProgressBar() - GlobalScope.launch(Dispatchers.IO) { - if(isAdded) { - if (!isInverted) { - val bmpMonochrome = Bitmap.createBitmap(getScanActivity().croppedImage!!.width, getScanActivity().croppedImage!!.height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bmpMonochrome) - val ma = ColorMatrix() - ma.setSaturation(0f) - val paint = Paint() - paint.colorFilter = ColorMatrixColorFilter(ma) - getScanActivity().croppedImage?.let { canvas.drawBitmap(it, 0f, 0f, paint) } - getScanActivity().transformedImage = - bmpMonochrome.config?.let { bmpMonochrome.copy(it, true) } - getScanActivity().runOnUiThread { - hideProgressBar() - binding.imagePreview.setImageBitmap(getScanActivity().transformedImage) - } - } else { - getScanActivity().runOnUiThread { - hideProgressBar() - binding.imagePreview.setImageBitmap(getScanActivity().croppedImage) - } - } - isInverted = !isInverted - Log.d(TAG, "ZDCgrayscale ends ${System.currentTimeMillis()}") - } - } - } - - private fun selectFinalScannerResults() { - getScanActivity().finalScannerResult() - } - - companion object { - private val TAG = ImageProcessingFragment::class.simpleName - private const val ANGLE_OF_ROTATION = 90 - - fun newInstance(): ImageProcessingFragment { - return ImageProcessingFragment() - } - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/scan/InternalScanActivity.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/scan/InternalScanActivity.kt deleted file mode 100644 index 6ae5012f43..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/scan/InternalScanActivity.kt +++ /dev/null @@ -1,196 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.scan - -import android.graphics.Bitmap -import android.os.Bundle -import android.util.Log -import android.widget.FrameLayout -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentTransaction -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.common.extensions.hide -import com.zynksoftware.documentscanner.common.extensions.show -import com.zynksoftware.documentscanner.manager.SessionManager -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.model.ScannerResults -import com.zynksoftware.documentscanner.ui.camerascreen.CameraScreenFragment -import com.zynksoftware.documentscanner.ui.components.ProgressView -import com.zynksoftware.documentscanner.ui.imagecrop.ImageCropFragment -import com.zynksoftware.documentscanner.ui.imageprocessing.ImageProcessingFragment -import id.zelory.compressor.Compressor -import id.zelory.compressor.constraint.format -import id.zelory.compressor.constraint.quality -import id.zelory.compressor.constraint.size -import id.zelory.compressor.extension -import id.zelory.compressor.saveBitmap -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import java.io.File - -abstract class InternalScanActivity : AppCompatActivity() { - - abstract fun onError(error: DocumentScannerErrorModel) - abstract fun onSuccess(scannerResults: ScannerResults) - abstract fun onClose() - - companion object { - private val TAG = InternalScanActivity::class.simpleName - internal const val CAMERA_SCREEN_FRAGMENT_TAG = "CameraScreenFragmentTag" - internal const val IMAGE_CROP_FRAGMENT_TAG = "ImageCropFragmentTag" - internal const val IMAGE_PROCESSING_FRAGMENT_TAG = "ImageProcessingFragmentTag" - internal const val ORIGINAL_IMAGE_NAME = "original" - internal const val CROPPED_IMAGE_NAME = "cropped" - internal const val TRANSFORMED_IMAGE_NAME = "transformed" - internal const val NOT_INITIALIZED = -1L - } - - internal lateinit var originalImageFile: File - internal var croppedImage: Bitmap? = null - internal var transformedImage: Bitmap? = null - private var imageQuality: Int = 100 - private var imageSize: Long = NOT_INITIALIZED - private lateinit var imageType: Bitmap.CompressFormat - internal var shouldCallOnClose = true - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val sessionManager = SessionManager(this) - imageType = sessionManager.getImageType() - imageSize = sessionManager.getImageSize() - imageQuality = sessionManager.getImageQuality() - reInitOriginalImageFile() - } - - internal fun reInitOriginalImageFile() { - originalImageFile = File(filesDir, "${ORIGINAL_IMAGE_NAME}_${System.currentTimeMillis()}.${imageType.extension()}") - } - - private fun showCameraScreen() { - val cameraScreenFragment = CameraScreenFragment.newInstance() - addFragmentToBackStack(cameraScreenFragment, CAMERA_SCREEN_FRAGMENT_TAG) - } - - internal fun showImageCropFragment() { - val imageCropFragment = ImageCropFragment.newInstance() - addFragmentToBackStack(imageCropFragment, IMAGE_CROP_FRAGMENT_TAG) - } - - internal fun showImageProcessingFragment() { - val imageProcessingFragment = ImageProcessingFragment.newInstance() - addFragmentToBackStack(imageProcessingFragment, IMAGE_PROCESSING_FRAGMENT_TAG) - } - - internal fun closeCurrentFragment() { - supportFragmentManager.popBackStackImmediate() - } - - private fun addFragmentToBackStack(fragment: Fragment, fragmentTag: String) { - val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction() - fragmentTransaction.replace(R.id.zdcContent, fragment, fragmentTag) - if (supportFragmentManager.findFragmentByTag(fragmentTag) == null) { - fragmentTransaction.addToBackStack(fragmentTag) - } - fragmentTransaction.commit() - } - - internal fun finalScannerResult() { - findViewById(R.id.zdcContent).hide() - compressFiles() - } - - private fun compressFiles() { - Log.d(TAG, "ZDCcompress starts ${System.currentTimeMillis()}") - findViewById(R.id.zdcProgressView).show() - GlobalScope.launch(Dispatchers.IO) { - var croppedImageFile: File? = null - croppedImage?.let { - croppedImageFile = File(filesDir, "${CROPPED_IMAGE_NAME}_${System.currentTimeMillis()}.${imageType.extension()}") - saveBitmap(it, croppedImageFile!!, imageType, imageQuality) - } - - var transformedImageFile: File? = null - transformedImage?.let { - transformedImageFile = File(filesDir, "${TRANSFORMED_IMAGE_NAME}_${System.currentTimeMillis()}.${imageType.extension()}") - saveBitmap(it, transformedImageFile!!, imageType, imageQuality) - } - - originalImageFile = Compressor.compress(this@InternalScanActivity, originalImageFile) { - quality(imageQuality) - if (imageSize != NOT_INITIALIZED) size(imageSize) - format(imageType) - } - - croppedImageFile = croppedImageFile?.let { - Compressor.compress(this@InternalScanActivity, it) { - quality(imageQuality) - if (imageSize != NOT_INITIALIZED) size(imageSize) - format(imageType) - } - } - - transformedImageFile = transformedImageFile?.let { - Compressor.compress(this@InternalScanActivity, it) { - quality(imageQuality) - if (imageSize != NOT_INITIALIZED) size(imageSize) - format(imageType) - } - } - - val scannerResults = ScannerResults(originalImageFile, croppedImageFile, transformedImageFile) - runOnUiThread { - findViewById(R.id.zdcProgressView).hide() - shouldCallOnClose = false - supportFragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - shouldCallOnClose = true - onSuccess(scannerResults) - Log.d(TAG, "ZDCcompress ends ${System.currentTimeMillis()}") - } - } - } - - internal fun addFragmentContentLayoutInternal() { - val frameLayout = FrameLayout(this) - frameLayout.id = R.id.zdcContent - addContentView( - frameLayout, FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT - ) - ) - - val progressView = ProgressView(this) - progressView.id = R.id.zdcProgressView - addContentView( - progressView, FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT - ) - ) - - progressView.hide() - - showCameraScreen() - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/drawable/camera_button_circle.xml b/libs/DocumentScanner/src/main/res/drawable/camera_button_circle.xml deleted file mode 100644 index 5bf8734e68..0000000000 --- a/libs/DocumentScanner/src/main/res/drawable/camera_button_circle.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/drawable/crop_corner_circle.xml b/libs/DocumentScanner/src/main/res/drawable/crop_corner_circle.xml deleted file mode 100644 index c42fe5877a..0000000000 --- a/libs/DocumentScanner/src/main/res/drawable/crop_corner_circle.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/drawable/iconclose.png b/libs/DocumentScanner/src/main/res/drawable/iconclose.png deleted file mode 100644 index 0297199d79c4182066a18a1049fb8673f3f8fe0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2456 zcmaKu2~bn#7Jw63F9_&;MOvz|29TvtEfz!@67fM2Fkuk`0Yjcx5JY?mLA1@Ka4on( z4Pj9vM6FvvP>@X}kXS`2gbD~!1Ej3V7M5W4yzBU;@6EiKcjupbzJJa=%YWvaf9`3@ z?m#mWYZDX-Wkv`J2t%RJ2%=HOdWcA_q(&g(C?z;z7a{`!{E>x#mDM^l8f|ZHi!kya z{1N{5{r^>lhK7Hc{P&k1IS73=15snr!ghzF^bOUk`i(YPXLMPmA3K_A+kN7tOgQ+i zXW!N!i$*x&DqE85mJ+>ejY1jM5CU)!#|OvW%trS3ugQ{>##b8KQ)#ZYbo`udomuCN z@O2hxD-NZla|*58wV1X_WKOR6sV`8! zF~%T5o|%q=15(u%zl08-KUvfKz(u8~k#~D)S(d)tTCA zzAn&jmQUa(SsF#&0~U_=)z z+dq&cO%BAY){;X>iFtC9BjNZJ=!AiLVl{dnv0W?ryrx1N+ZqB`NS9(OB)+XAz~X(z zWvu^0LiYz;ax|XqPmsIVsVt}sLPHp9*6hM-9cCu4%GQutS%87|?zm}|(&W)Mo!*ATsXCX2`d!*PUoEPO#h2>(0hMZJtr*>dDO5lftN?3!f1!6=Bm58S zVJ=Cnuq!iVj$Ez>os?^zVpnn@AIz(*SC=XtmHeX}bro|^?ep}f;_I0FSkwwE)j!c5()1C}V zY_T-vBw(t#jiX`QX9Mf@2)={11B0#%9ON_L%4ors>!)yP%07s~nhF~S{%P>lGR=h% z!awy9ys_SiJXogATSuf0iP#mVz{rK%oKs*CQYl2?tzK}v)dcTRf2y!7K4Q%u0=wqO zF1#%e9tcs~#y!TA3G#0>z}}kez&j7d0mx^{gAsxyXP+RIj24r#l|<_6{GaHtfJ_li zocfJR!6#^8&{`UvGf4EQvI(UDHy4&1Z&ksYb)q!LC(%vv6{}qT0mthZcMFiTNfOTr zUOEPx@*8x|ogDQmMa|7%k@hD2Wk zwQHFEXKhGw_ME|8wdNudbw%;Fk9izmB^_A=?<_iWQSfF+%mf#nH3XI1glkoqVA*ZB ziM4?p7YWp=U(CS-%ykbhaEHH*;hqS<}AcK^c{Fky(qfSNzT4U@6^3`fCcU> z&41&!a>O|v5}2>A#?G`N9*r&x*f3jH96wcA7J11-uR(9nwd}Z=y;Q+J!QOWk8)|%k zrz{R;_0eCo_w**_;Cu9RAYCOiUw;pqHj<}d7|jGD{?maRn}o%NszjsU;B0$HxAVT< zIwxEy^tdqHU+3tu#4QivKMPPfx-4^X4$6KR$v^CS=TB7Q!wK+M2UjX)cBv(``w2#6 zinWwxy=~5I@z^zINVkrFg{sNVBYw&g;T59EKN6wB^ z|4Pb>|Bc=Z;|{me8{ri-0h2b4ROG`XUQu*#3am;Ot=#NXzZddjko4-tMPD$Gi(tvh z)$U>xR0MIuZd?96PLo9iR-efdijbha+~w2`ogWY_OW+{c zc4#X>dta$@F~XzTbD&cC!JbclX+Jk-`9b&P;a6$x!?=Au)z(ClkTlF;@OgOOv?rJZqgDqaT|d#PTot!@O(48N{# zHJP4Kr?cN=aev5p!XX^b`u2@+r*|00s4&BOiU7oa*yXy70?WD*Q ze6oOP`t)fE?Pq?l;Nn*QFo!N&-<0RYnu^CKydHr2Dc;IrU&_m1zIxMC3dK@6X-Tm; z$jfohH_*^tGn?w5Aq)S}%4}DSld5Oi+tK)aQekM$*9`eVu(|lq*}k1N8bw-3_R!O3 zt}D?-uSTql)p1unUJkbHe9^-I(?%a*ww>Rdv_P((cz!8p3$s5fW~H-p)l|zL*tv&8 zdWFMWMf#mY#%Qzn?lCj;Ce3Xcsioj@iK5T7s`TKK1eOUpEdHA8N5wvG%kj;1MT-Zm i9p~$aITa2I4j$fEeK_ zMvcb6XlgW2vqn)WV%4A$MR^@jNlopd7^x6=HvT{nBx-~Ngx+24OlO>SI-PX7f86ih zbIy0p_ukCAGy9<-Io3huDgywA__(MP0E%oWU};j$?Dc9PrNodpH=5M3|KIQa!F5~6 z1m5{6$*Isftl6$w;8Wx8aPh+6xU+376Jk=5ua3XBB_l0d$5P#=a;jIBWUtz%I63!S@G1uE&o$r>(eEhEWmzD(@*c~SCczNEfsufjN$G%q#H{4!# z`b;lGgyQ3?OLD;ulmZ+=S_lPNurq=P3UI>+)Bf<^kiu!ozodFS7?5^$L_CH#QtKuo z3MAa)zfg=|&);iOagYdEW7U8Rl&LF1t^zOke&wbGtO`8g@+j$2q3u>R&+_-W)g16d zy%SKIBRM*A_yOARWSC+MVQ{!K6T^Nf*GD|@HPkZvqJ%ZSL)5bfJsl2@y@9A2S6p8@ zAHy;`?tb?K4sS&1{nlEH;)XLx+cM&Il~wU|&^+49u;vufew4INh40^dtC#pC5MUYs zJc(Zj@l)aU(po_GBFOm(GjjCj!FFPCUoQ@|?eET(a*yW=BL*}S9xVChV`sd$=&Lkg zJyoCs$&K9-9{rSq48lG9k0t*b&>LU=HE2xXxz83=tWro~MtqD-$iSCLdE_t=Bo^8X z#h~FGG1Y?%5C$?cVIF}V|1u;=)b+}1+(_lzGjoZ`LmOaT{z25^rkR}KPVZRe z#;pw_{R_{uR;GvKAqC!X1nk7=i;+TjC4(%b`zbB~Zg zh5T>}@+FOx#icM;Qgv#3u{$K8@+p~8K1KMnmBMCRJ9YPD2t%L8)IuB6817bwR89lN zdc-FPm968!1Km#D?G8(rPM1E)JPCX9FMt~95c&LAku^{pM<9E?gg_hNlAJ)OVj;w0 zSKLX3Qf+9@cvJRVNkuw=robpQC4d<)U6OIXhmWBS-jRu{Mafl@0}WmpL2cCHeA1?% zE0W;F^JQqyI6*BwA`fX|v_a&Nb?BV+?Zo2c1^u1JR_oa{J+2R$f&FTnm;V@ZcKF)z z$6FTk+|}+3(i>Y~9Q6?_0ech!fp{&sn_L7pCbxHNW}S?W#A>V%uTbHe!I!R385xd_ zWBsWL}n}9{^64*yUI8bhNeNFMbwJUTjqHt@ZE1UlghlKg%<9{u(tZN}%mBlz`+pf@isC*<*| zAj|0p0?8+NM6s}vHV6?X^K+eMs-k8-%H0!`Jj6=NMx4A>-`VkY_Sr>GiUS(;n?MKc;i` z63N5+Nas%}L~Nf0QUyncn#*0p!5@Dx$b&I`q6XEs@yGhs&&hmo9I31bqHBgrO;G4azpF`eLnbcT3)5l1G1^uSF*rx?hlF%{pw zj0OuA$n>}>BBJf6L8-FRfS~{_|=k<4B@A|nW%avJnTb?BYd|l zRfuIq{AeM`nF_on5lGY8dMS4NT#mf^khn{Ryk7!^Cs0|}kkvTr0o_m=$tM>;M}`Nd zpMfuslq-<#j#L$&xCyJAsV#xDCC`KN3Br{mCP?rOf~=i#fj!hFp?0?NwxL2Y2-rO3Jn`(jp(UOjfq3xv2UuJ}4yvkeNut-j zo5Zzs=?W5)|C)97-=z_F$-c7$zP{9&Ui1`Jc;XCU9X|gVEnX#tH=cSddY|Eiho4bm zInf_pDiG#&Fe7x6*&{D+AlUioG#_Lg*}(;=Ou0^=vu1BU_YoxBW`~X=P?iI`VOANL zRCoCTU6gNqJFYKLoOoxEPDmv_UtBswe7Z*9^tW5di{9*p3RH&{YTV-<#=uG3x1e=b zu!+f$n8sFJYE}zA#+3{5LpSTtYpAodJosH3<9^AaHaiPXo-Dn~T4dtr6mdNsC1gn2KvHO$(mwF<{d(NK!h$z_t<>@MHPlop$5ulF0av2JRD ztfphj{^VjCH$bx4Iz`jAEiqQh%!E0fye}Rr_4R(srvE?_V$j#Pi6#;<$lGhAwdJ8IdQMW8*MYOM1vVz^fmCZe3M=yIs9 z(xOF$SxnJ)AF9PSR~9BRz`h#riQXtQ%@0Yx%dK!7-PUzw^Ca96j4%2|>8XY}jA=J3 z#;64Xx<$kEjqtVxb^c3Y6n;MPhp7Kl{nVx-8jgSY;Pr0D=ry(TdS4^|10X&+IqG1f Han0WW2br0D diff --git a/libs/DocumentScanner/src/main/res/drawable/zdc_flash_on.png b/libs/DocumentScanner/src/main/res/drawable/zdc_flash_on.png deleted file mode 100644 index 7dd46e03b2021c78a8d00728378abb2c58ec2cfa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2260 zcmeH}Z%i9?7{{Nx9`x2xxG6D!f+ft^7bYuX&ZG#I3IdB@$C$|IkbxrEuwtw*LCZOd z!RdhLGUphiaj}g~ah9o*h0+A4&Mo^B*ispE&8VyhZ0H;yf7CBG4RiX&#F)KU)B8N% z-}m`Gz3cDy=3X;orz;gv3L;WwW~Am2v5+kBGL&xn(r-{!86M6}L;daFe|{|aY8I1A z%sJU>Nt5E;>r+%0{hy^NAnA@2S1@cyoRbCW4* z=kdEg8i?-vG;#0eW=0GXXo7B*8*2;3J?)J>3Rb1q8rL-0TrASKk6FevyvxL~u4LSF#5BY>2j0Prbd} zb=W%?Cn9i9aza-Q-`}JJ9Yx`+Kx6{?UGs_>)P*sqefxeQo2WUR}jZ%y$n1dd6JU69(ZNlz@~Pfd;xD^S+N%| zoxoNGf&u?Bz$d9zxk#syLWyzKL=kJ9)DV|(X(RDX-fj|9o%4@nnM8WZ6fBZ*KuL_E zBbr!$)qOaJKq(I7InKRz7(vHn1hX6n zwjM_?ttEt*-CErL11(g?0;Wq&ByXc&mblPgvm6(;1?gm61Hn8~J|?r0b0?#x$44ku zYi+G(0v!7;tUf?fz6na^UR(kjQ4rb+ z2dXgMmw@rH7>rjUR-cI2u@=OJc&2{|doYfxNauYGf;&1^Ow?1Al`h_F{B27bCPsJh tM8SGb{OPX2hUIK=+zvARKi@p|n9MkAtxUE46B)`9$`bi{fBK&HPk5hSo_p_e?m5rB=iDD|qJ{ZIIF|?)003}P6GJNi03C)P zfP?Lje99ec4ynSz%*N=D5B_(coV2NM=vXVr%G?@&%Kp7#SK-*cwXV#`pF6cU@=kcZ zhh%lG@70kw5A=8_Zn^MbOT9W@R)V_rGDEEt_JhmnVj2 zh|C4;6`y~*M;Ui)`d28eK!kgJcJsZJ6}AlSbWw=EijN=aS}B$l?6h)&v~2#g4n12b z3{QHHoBCrh$8vvXUioOu+0z{jdw$vPMkQ(RVPn*1rrJYzZ7KyH&*)n5EFJXD#`hF> zT)s1nTHuJ8B9`)Af5@4!az9VPW`EMVYC>gnYY6pN#?K%)b4RbXfidiRCgA3DOei6o5YOtd5NVg~)kq8e|E8_E&$i5b&*XnSC_Q(2YO#)n9 z8!ZdZm~$deJUHc{`cAVW?pjnP17$yRYh6gb%gCb^e z;NnK1i>pnVNRuzqN-V`t+#l)W9V~JpXO8Jm|7g{h?Sgcg1CnUkX*oGm2lF2>RjbyV z3Z2(`uUFMIdQLD}Hr7ZfwRe>{z;~oX-+qxklE0SNd73w0lNJPb8G}x;6`^)K1CW)8 zqJ8&PdRv%1c*-Al@Hpqyg_WnjvmL`vO8z) zp<0#X7zj78;(9CKZqaeY#yQN#9>jlNt76(nwA6Hp=8qR3_RcaC8G@+>`IX_zb~vS5 zx%vGFFZ9$+f4DrX#{Y4T)6oi{r09>qTW6EF>|@<1JXz+s2pmIz4Gla-H$|IT94VY# z8kXsex`@iW9nZRS^+n~6(GUxTC53zgMU`QXZ$9ypQivz2b#f6w73yV?v*{DEt<`CA zAO2M6%&j1-T_l$>xz{z``fHSrSvGA$cfdrGi123DR6gF$(*Ac&zx@0i4V=B36i@D| z4qT7ral5hb*A8RLszkWWvm)*&<|gS#VLwZ|gpd@kMoGI59%y}}B->CuH=oUmhp6a& ze9NwWufpY*)Sh;c~EuY|Eggll1Ya_Q>TR_j^CJTc``~2U@+07pinG zt|F`D4IQDA&lakp3=C~iXOgImUhr(qCmU!f^X`8>BW187&-;i&zeFO_8jV}t4-c~B z%#UF{uhS@LIL4_fzn)0;Bkt@j#FYf*=C8pA7w+!q#Xfw2cmT6c%dh-ieIoLFPD<%7 zB5dU+Te0>@9*2J&fX2VyV#1{D@IKxn6fV!dY-~NGS9^4z7sqIT)6ICgwJzG9DamM1 zANB`VwU!fvN#i2;na_@2xFkrYv%5ZmEqWuK58t{R3Y?+t29*KzM&V|Jrw`fW+HBuh z0wYKjMY}>6<)!}!fgfYsUGh|o?e4M2p)26AS8M5K=b=@=Fij8Wg-vBb@t`+!X#yV! zu6rq|90xk0%qQX{DX$XpaL(mU^!&7QY0zKi*7q$UUoa`*D3i{I_%(%1j#S+a$8mS9Z5e-2VF3&rZ5B2FwO-ruY!D3sv{6s|*}Ql6?l{X( zm-N^zV6`0QNTSZA%#AJH^T3IefEO}r$xZ4L<|klnYdtV)0`5;|Ij7SYKgavVgXB7G zZEX)!jkMHr@`g)&Qg=(t5Lhlg8flck^R zzk+YxcxyughofED(W7&LS1JL9U}>Qu$^5!{nVNip(?@Xq4Hrx=n8od_v{C#r8VlNN znU(Ya1u3;@%pkD$_l%!H2X1P-s3(=0>I577q6}Kq0=wk#LS=xJKK+PuinQGk#LY^V zGk3x8bLxgusw5F{lxx5~=iX;1MwC-#H{o5dH2Py5w{ZHr)gm|R+wqK#EfcHRCX3M_ zAf9U~kDGyPrj}mG>D`FUOPu}kLcgMArsV|MqvI48<8t=H*2Mijb3kdwI6aK;4Z5Vo7pTOPQBXJe6cE4QFnMWjITX|c zXuWwR-sKH33FuNb;R&lfP40Z%hOIm{GYWNLB)a-lZfqfY+gA!!TaO9cOaOyitk^jr zFMkw6w9?zl{9N3{+JgHub>lm8ipFS{==JyYvBHJ1{a-nUn1Xo|K9CgTBOF5iyi#s@ ze5bbNIWrad4+?T#yO6#BSej!F$C42oP{>m{BjoonASuA1vEJfBH7W1@y5j^g>PIG% zNmd!qOBRawo#|~6WZabl0e8$EpzU46Y0a*iP$*0iL|k7Na~pkb!Vc#(Y|JFCb^h z6QoZ_U9e&gfF`t0H@u zdGBB3fKf3y&$xQrNVTQM=8qH8@WuXY^g|N-EhHNFlogdr1z&7=>ry8d;al8#1+Z4n2(MET7h$Y-2SP@Qqx z7Bbkc{np1@#3~4`qyaQ6lnTDtATkk^HZ|lZv+Mn}OocAR8)S$ zTczA66YT;?-0~tC6kDSRsxXpGDJ%&@=0;NUYY?HMXnU z>4K>77~@jk6uzK_wJf_q!Ou;J(1CgjtQ{sM+OAC=OSu(%J*@JZ3p_TMa2q6;TlIjS zZ&UBwPw26{{}c)Z)e9j%!>J$xn}SR)iUB}8*am|Nm-C*H6W6obErxI4ta+#d5+x zS4@`c?na$Tw(#MpYsC8U?o&Nd+)KmW_o=K1>swF$8Av;Kek~)4jbcw)7%ya|3!b7r^M_q>EX&hGj1i|OZ=jn{$u2H6N zj|&d))GFiV4T_vokYlUn74 z;TH_T3lG@lK>oreRh|5O7os4$A>>-0_XgIv?V^EaC3e)`TfRKTyOP-$Qvx|s=xy|! z;9F7|@Z$e%;|q(Kbn_ZvR^{6t6h8_q=g_ z0ESt$h6(@Hi1#Znu}D#H%DMZ-V+^9D94~-7J8R)tvSJnGTAatW8Sl0lERaIBL~$9n i$_YaYaQ~l}06@LR9ryHCkN)$^0j5Ugh86la^8W#nK{l8G diff --git a/libs/DocumentScanner/src/main/res/drawable/zdc_magic_wand_icon.png b/libs/DocumentScanner/src/main/res/drawable/zdc_magic_wand_icon.png deleted file mode 100644 index 68a9b36a04bc39ae597359ff8823185345cf760a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2344 zcmd6pdo+|=AIJARo?%=jw`O=0Zk|q3{=c=<6vG6mCyV%l?jAq{L+u(~ zJ`Q@V4XUE`YbcdB#<#$8q;CK>?X*1FOh zJd^vU1_Vd?5fxcJ#b%z@yoctruBw=DU{PGnDIm6<54x;m8`4X)?{B@q9&Qh*RVUsF z{Kqz;nM!6buyrqNk1?C>c=l>F3_9;X4Q348{isn6RL##-A1yE|w5*h+;eP2|3S1Gm zkP-IBch-qPn zi{YP^8b%uYF`X1u(rwP|2L>wzNLUQ>0w@o=?gyJ+a+fJSMn*KBBvytuRoTi@xo+s! z4rR$h+0IfA!(2ynwjIXZa!>rAo?v~TAm9dr$QSLxv|I+I)8=v%N^au98DUR}w8jRc zeu$ZIp}En#al-I|Zn%vcmFtNz;=;QVWVg)uXLY?l;+tE3i^}yz8Ql2|N%q$E29XK4 z6756d2vc13GdLdZ@g`+V5^-PoS#=?xa^sbP#(8b#4*WeK+Yc2Mv|t6py)_=rJa&n! z=3{Cvt~E8JIYZ4wy8u{O-DmM?Yb(xc$i0pMR7cX>>a#I z`W3M0*4L*Q1jeT%#czF>zU4klr^pD4%X@Fuyw_iT;SdKdSz3j}{sC74{PNOhJ<9p6 zgdd&6Gmxyw)hNb7m-la@^XeqjU{Lq zRvj6?M7cI9E%x|iMg<3x$;rK*m-8k13Z;#rmOL}>DN=i}8wJSuV+M$K$#l;_Ej9#v zm|XO8NqIO%v@`~_{D2WJ$rAu><}6(Lqj1e)03CypxHRkE8WXec zIaCoz6z-U4MN!3EaS=D@9zfrW1Qz5=;@8ONxUuheOHPLPh(rpfZeobrJDoKRFn!{! z>x`oXhQhj~?*6B`+4LQ}*VIt>iHXJOwIp(Wl-V1x?LK+hMX}X$h`bWtx|V4M+-T>VxXQ zP3#Mq6aLCoSo%h~9E0eJZt&vX`LR%z1qgzQhmBNZQ5rh?JN7Y9!7-S5h$b+{_%Mk$8J-cT{S|Z^)WbnbKFB@Y ze(r(05GGXIR+CC7f;d@Tia-%Ja}u80?q~#4xxT3O z(Ec}irr#p#j-j)Pa1Z%sMFz7RiD|htnWS7ReTkEc%D9MhRss`mV;WS)<$VFpt$Jz) zzED*5_e@<|?>8Kvkz31z|I-uA@;O=xJ!wk5b4B}K?%Qd3(zOZ3S(VIfdexem%Su9z zYNt065)uwn*OInEQIttbZ>EkvHiVTbkNk_h4^KWdP_MRi zwrtZ6F?Ls-s%I0l^@l>7}nSkvId^=LC{W&Z0~(*e1-*0aZil1)!_R z%p9=O%r%wSXcW67nX;mr`W?+&THNm&%oa z%e=Q$8rkE|SGsM~A=hFKi;N?o0Q%o2Q?;&(>1xTdhFw{!^L<6rHdW85EX|O!rG%Pq z3aMM1Nm+eil3RBzoz~V5awjL!Db@4(6#g}?>3=!MU5%e9@*Tc&`fE=tl-VttKwf{m zPIqC)c1tP;tnXt~WCKW48M9y58LNGcst-S3o3nacrftHWmMGS*%Kt5@(V28FC@TGL pAu(apuX^zHVkK+qpEV3hz!mGG$5rkt(Ej@GxH`GhIJ*Ke{seZW+42AY diff --git a/libs/DocumentScanner/src/main/res/drawable/zdc_rotation_icon.png b/libs/DocumentScanner/src/main/res/drawable/zdc_rotation_icon.png deleted file mode 100644 index 2f8a18d82b37d601f65a587b0029822f8490815c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3847 zcmdT{`8O1P7yitc%`zD?_H_o?#xfa25fie9>`ECzqC_QYnz0k2sANrcEz154Gecx6 zDurZU5;NbVl#qG7KfHg$d(XM&d7g8hd(J)g++S{ry{$Q#Pm&J+fVQ+SbpQaeOCbQo zvkQTb+?;ly#@@#1=q~=hgwFnY)7@G1t~l5_f-NYV=0p6H`| ztz>ygT<~1z2_>=K=5#xwC0W=Z}9#r@8eDju4-_IJbCG^$rN5&`H zMPTBiE5lR!0X8yL0N?a$3m8^v;TBkOnuM18!UXOqT{u4{g3WlXxLSA${NTHsyun49 zv$m(ckOfI&wn%AJPjCrI;{h}tCS)1}0}Ck{y+|gPEe3>|g&{_mQ6hjcCKEXobFbmQ z_-L50pXyKZ_}M~$VEESR^_$m`Qv7T|W^2q3C}IuR{8jSpP8<^hj*Me&9f@POUE=n? zq|I+@3E}_j)KFxZ@3y$_hOESy4u56R1SkSZ^3ifrkUPuQHD}H@@FX)rR;7W#m38y; zkl455ChHHKy=#~&!Ujlu8g{)CIx4YGJ-^i>f0A78er9?W=;>HeWmv;eXe`*28hKF_l|b_B%x6(0+r zB5OqnOCDX;CajA944!C_bwk*{k3Qs1(0tm;IcBaw~Md&vvur32k zMrS+?j{27OY9gBR?}Fl$&~Y2CfVu8v=oEx$N&kGe0N=H~eX645{7gnCCS0&qJ#$b4 zY>*q3SI%x=FE$KU+Ve0 zvgY?wj45A;S;bAQ;Em>m~`3gBM|w%y)p2X06M&dn&u77zoCRH4B(aq7A3eE7TA` z{V6(v?Y!(5i!=g1_BI<(7399QqmjrJiK?jr1 zfFGXR?}A6}DzmoAW0Cj>5Tt6qY3hvx@t!WzSaodil|a$XRY^rb5Fbg^jhBZod&d3_ ziK}BkGTSR+Mp_X=3#pzE>_EZ5OjkXH0mt$NPaWt=V?uYG$|opzz+{}_w}4Sw|0|e} z;Mo@-DIkqF4*gPW1L{-1?vyKyiT1>i0iag$lwE1QY7`w8# z)I$C4Wd~p!63j_tfRnFpsk#nhCXeeGt1X{2AJmjDe+kLBM{B^fVWiliK-3*=HbFj~l2KUP$x4TwsbcT%E7)%U{P zlqlyLjTt^y0qx{tzq6g%%13^`?d^E;8=?;h?x(oj;fUT_1`c>(L|KNf!{G(JT@2))g|do+6N`3E%Dk3 zo=2Jxlh5h1P0#0|ps^8bHi*hyApu?r~;lv|0W*UQni<--Oy8H4L=NN^e)QJYX z-TW#|NjwP+oMgA^^gb-j{%-ErD6zQx(5bspj20F%@Qpecb0bRf=(qi-?&BXL9-iyI2`(RK& zWh*^6rAvH>quZ%mNiJsVzErK=w+mgwNBi&`-ENISA6l)!r;Be|DEag*E7Z)R!(_X} zrTvwLQ=cLCCGkS;8Rdz+N7q%~#CYP(C?QW3JW$P;V8XO9Gl6y+ORIcp=Yaptby{V^ zinliud)?ukf~OYPaIurD(=P@bi^AIO!C3si_${d4Ez9(u23Evv73bKW3;J@A7Lp@W zmH%Ry%~lCV!VUGRH>#^phgT}sWW2?nKeRF`Ut8oSe8hBa9WR@>Q8lb@o@0U&4w5GL zonrnFD&`C73l=HW3AWiNj9zLyrlw`(Yo&H)bKRgwQP-+WbTLQ9TBKbu=Yhl6?Yt#uz4TT23hfg{ zDZ0^jejwJqa1vvQoad!|QuHBQTB7L9t|9&7%ExcK{OU^g^Z(`|cmFq@?xVF&{>9~& z$u6zo51kqb3ewqan?CQAxAY5bE;(I<7RHE(8#UW1hvc0LI*5KD61Dtm`p^!--ts{E zoU9TpK%+q_qv7Ut=b@0#ufkC@UA^kzS+()9xydPr87Fv@y{>+8rdRxSKbdi3G0b;L zh8rj%ZTHiu?z%mTxQyR(fJLSroKDmuJy8vB)%DS_omy{_<>8hPvC=EQb%bwT_C z*wE=)jd|r*%F(U(%NL%Tl#Y$3L0<30dnnrc3!zDC8M62xVo*((2a1#f=JWGxz`LXt zDMI|t(36PnUm#v`Yr!~A+zNDbh@|cFlClCxVn>dRU&E{hNkZOVUqulDxV|lK+-#tj zO=Hqx@H?lOtFtAVB7@Cz)7+UwaOe`#>!0gI%IEQN;F<0QIeX z)bgl8@v^Bq3(9nhXkz=@q?!Y>>+~LhL;j9csnU1;w3vKnBd2E!%qmjdXDYp8h*=Xc zO{YzvhNOVAXK4;P)jB)5%zKq0V=?jn9DO|io>>j+^uY+Dm$Nt(f1)|5Hnp=0>KC%m zc5U9{c4QW3dy8q?IeqZpU8IS=$jVnK;fWY;yuxg%+ra!9*GHH0m4x1e!y*;ebG`FC zM?V$-VQ()@9@K5po>v}2wi&s7w6uqav(*m8VA$}8+ITK*keQM7Nu@5pT}e45LiA3Tm6WA<%j*OT5ll@Djz&d&P*}lk>Eo|SC z03V%dPSEy5d~845CPsoPqBo)!j6Tk+;Fa_j&H%_kusGw%6$xt8JR$DZjmgbVoghC` zKZ)#to6^RV)lnyp=vS|-q3NyS;bP{G?o-t;&@FrN$j&X<^o4(t=A`@ecC1%=`@Ww) zAFD(jI`QqZuh;;*%R;|go6=G#zMwvc2aOZy7{9g1#3vHQjWt-ZhW5ZeuCfO{%h!GL0DyB-R08+jaLT6S@g$&1Cq(ialVb; zg)fHw^1!@S;``-EhjtX7ihkNB8x|tr-=i_Oer-4|F#3b3trGH($+ zJ@@L$a`jD7$|Uk0|90?s{kiYHA%h8+B>fIZ!sM62)Xe>1KYj0yJ{TP}f{@K-7Xxd* zn&!4teGcKuW03aTSK%v4p&yL+=T)*EQP#gld$t zR!;GAi99MdL9FA2T&DvRi-|~T^I)|PM^1*+L@t^b%@gE( z|E7mooU~v-hq4^)j&@rcso4`F2ezF{T;Zfu(ZR diff --git a/libs/DocumentScanner/src/main/res/drawable/zdc_tick_icon.png b/libs/DocumentScanner/src/main/res/drawable/zdc_tick_icon.png deleted file mode 100644 index 15376f01c0796392ba3fd5b8a91ee4129e8c9843..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5541 zcmd^D`(IO6wqGZllBi&EKs4cD0*H<+Xm~h)APEpUK2SQN3~CF4sNgssNreLXvF7-w zfW}9CAT21&^tLLF!c;m%@o_8~s%YU}w4$SeranO>T5qGJId|=S`1$?;_wxAwd#}CL zcYW8|Ywf+0iudOH!E?CpZ~*X}G2Qeh02vNtpm4{3KbKy8ga6pVq#5%R__J2Aq#WnN z%+nVX0(kvIewnWmHwL0mQqLQ3dOQEEsWYY4LD++US%u6Q~tjeo?7Vit- z6Ewq=IIpzk>Fv1fMLp@ZyXltLB(+@S6(QHFxa_!W|Lz^~<{Au4WHOGI+>InX*`^nzZhHHn9o&2ETx(wF5e%9{>ynPrz{&WCiG6vx5 z1<<(@fZxmlM2!Qe3uG|B|Jy1XChco~o`rvIl&m#jBEG-B>h*8+Z_?9$yYh$|IDK3R zyw54CXh^@NJvDe07jL{;bNaxVZogBQv9vdT8H_d@UB76U3SeMUw9OLvS5^T~+9|i6 z)kMfJkMX@WM~}xC2DP6RJ#-dq-U67^l2Y?vbgPNSWcy8vbyWuj836>(B>UOeX~b%+ zF5Xo=H<$?Hihac^%Q+oDuV>s2@xdMi@wH{6%~DuOd^shlVU*Q!`I~xI zGJrnbt}Q%TUz^!*y>fs#i}wJ0X6Fpbbx}6Tr!w@z?Xt1B4`%{jX}@~D?!T6vl+(!o|*?B zr$^ML7>V4YirwoSJ8mqc-g9h*W#3YlEGlMF@+wp;))H_dBE~MEifIw9@G=Y@X%0i0 zozy1&duN+}0`((DnsX(Z6EJHxsmo{CgSm60*ta3g$JBc>de{3i6PvneD7!*r{Ez5z z9(8#W$#+rm`4^pSsj*Zs02QB7#XqCsRY?&Eb@pf z>{zgp1R?A~>s{2k5}8%fa2rS2wa0&^;7c)v`!t47gltk1;(8?6wTxO{wOUe-Q1C7! z`IKfb6-hSvQ$-}%RZJD5#CK)TyM%!CBm&gn{;I_| z%;j!10mGxnEF)mraR4K+9ju}^f~_ZDmR$fbji^K%pH(mr+CjK{qQKLUuHk;RV^!EJi87eKi`Z@T0$~-QvIsL-N!i_F zq0xg7_EUl2Mb-+E0J^YLZD%ruiG$!pA1GJ&j}jO^wUb}u~{@W4t{Te3ocze{51s78G({oKLT`|!LZnKEe6 zp^AV#94~{I{UnH6sQ?E_T(vC<=#3&^h0zSud`G~>1_9qnvewbcg86q8YztN!$)6$3 zF9GoGRg&M1Gj65Z37p9z0QR-tm#4pKuYXh4e*Z;YUXNS6jesrQ?8&1b?p3uzY^{HD zwEoTe9K0k^*B%OV>s>l1o>>>^^5bAPl{Md$^=@bOIpyVA6Hd6cB`^Q4lmpbu?m_+>Fu-~hGRO@qprGlK7Q z7MOq>@oLM+k7mNAHcPOq7jG0yLvyQJ_CdlxYNeWcozT6SFeoP7W;hrd3L||9gZ(t$ zherZ3eGi@Shtk#+l{NwDNBx7FtKkA}&1 zv3+U@!5>T7#*lm?1l;`yejH_MzQ^bv5w^F;NnqJL%9B4w+0yoj{KtGqjUb2xq_1lK zmLkjES@LVoELO-MJDyjXCKvMTVp~UTVGIY8scd-KTf$FvtW4TAhOW{W7_C8%b+kM` zyOhi{b`y_JX@Et~M)E1dV?Jq9%U4Ai5SoAWdJWKU}QEkBr1Mu~=;@*@@V?UCn1kG4wf(o-6ej;&9o5%?wwKe54`cUbl+ zvZs>F9#SgA=C{MQ&Jd$1r#ranNbFtl$jl(zO{nxK`Y_sfqnUR`jCk54>I zO=0!^q>dNd`LfKx}WmK2|@IJzqZ5AYmWBi8gYoU|Bcq zw5C@V;wXTl?TP9P(lkgZLIw8_Gqm^-ymz?o{+HBiU(vXXSAIZB8{#=G;m0F|fIM$ovj1 ztyA`6L*@l!XO-IKxFK_I65L&P@PkQ5fdap(#DK?k4_Inr749X!ZU(^InK-gGtyxu_j!Q zDDt0BN=yrD+^bM{F(XKoXp@RZNCAzf@v~2TCC90>7Zm@VCh@PLmtNz>WEssCKa66< zVW-mUsH;de8ox*$a)pmXj6NSS5;c=T9#Qre*2NLobfR~}N%@j<7tRGn|4{1*NJAEMzd{ila1 zzi+G(zlbhM;4eN5wcS*yAhuV7WHi2hSA-d%}GT5ev{Ub?^KD%C*RP< zh+4wZz1_NE423=jeTwqcZ&SMR z1P-(+CR~|9b1sZw1yA&mDft-iWXxaeQ#k*KU3z;I4c84n1(h^sHHd!>(%ro+5i8vd z&ASnDiUfHl1jg!Qi_er(X6u!i$FBF?h)(TWbz-owe%*rf9>cGd7k@ih@A|slg(u{3 ztHJEe42}3Ugu9G;AO^u^*tF%|AmmrUgkb5(D>$$af!31Zh%=NJUef2Jfym$i4dEvzvCUL?(B&T4^K^)$82D6$m zM{vRjyV8Fqco5&K?scdMQ)2&qn8(;qO9Nt#Kd#u1>x*BG(moMW2K2=l4vVzGZ6j=@W zP)4ue@J>>C%!btItVai`c>`V&co7MsSmC@$Fr%6$qmSkE9(HAF3WOTjnjf5QT^wvx zF$cq4!X8#gG6{E4EsSYYb7nlEQfEWxbhe+UdBavOrUli!Sm8U9vBn{<4201WWQ(na z#!*ZsuPFT-MZxg7icvdHJ0qvvbu@fz<9f| zNe8~(Opo4W^pn-aasw3(`9cn`M3nel(xLhvvEbHMvQQg^-L7m30cX+|SX{;xGQJ>+ zXf;iM(rdHW!PQ5{Fg9%+3(rXrX05=wm6rxW>GdQ;U=o7C*(oEVGeOXN@jMx21w!+U zO=NUO2hHs!IzrjMSu#q{LFxAb8Lh-oH%~|7p!AZFjGm$FCeYD1D31x3Sq*|43*i$? z#O+SdJrR&4q^aP1fC_o_&XDn${c0TPjL=ve5F|L=HYuRkheV|s2i=)EgfLb?uc|gh zu=M - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/layout/fragment_image_crop.xml b/libs/DocumentScanner/src/main/res/layout/fragment_image_crop.xml deleted file mode 100644 index 726f2648a0..0000000000 --- a/libs/DocumentScanner/src/main/res/layout/fragment_image_crop.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/layout/fragment_image_processing.xml b/libs/DocumentScanner/src/main/res/layout/fragment_image_processing.xml deleted file mode 100644 index 34901f292d..0000000000 --- a/libs/DocumentScanner/src/main/res/layout/fragment_image_processing.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/libs/DocumentScanner/src/main/res/layout/progress_layout.xml b/libs/DocumentScanner/src/main/res/layout/progress_layout.xml deleted file mode 100644 index 09ba456ec0..0000000000 --- a/libs/DocumentScanner/src/main/res/layout/progress_layout.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/layout/scan_surface_view.xml b/libs/DocumentScanner/src/main/res/layout/scan_surface_view.xml deleted file mode 100644 index f0274c73b4..0000000000 --- a/libs/DocumentScanner/src/main/res/layout/scan_surface_view.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values-night/colors.xml b/libs/DocumentScanner/src/main/res/values-night/colors.xml deleted file mode 100644 index 9444cbd565..0000000000 --- a/libs/DocumentScanner/src/main/res/values-night/colors.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - #232323 - #FFFFFF - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values/colors.xml b/libs/DocumentScanner/src/main/res/values/colors.xml deleted file mode 100644 index 1e7ce9d650..0000000000 --- a/libs/DocumentScanner/src/main/res/values/colors.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - #99000000 - #afadae - - #E9001C - - #77ffffff - - #FFFFFF - #000000 - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values/dimens.xml b/libs/DocumentScanner/src/main/res/values/dimens.xml deleted file mode 100644 index 6bd0df5ac0..0000000000 --- a/libs/DocumentScanner/src/main/res/values/dimens.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - 2dp - 10dp - - 12sp - 14sp - 18sp - 50dp - - 80dp - 40dp - 20dp - 12dp - 10dp - 8dp - - 100dp - 0.5dp - 0dp - 15dp - 50dp - 40dp - 32dp - 1dp - 2dp - 4dp - 20dp - 50dp - 40dp - -20dp - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values/ids.xml b/libs/DocumentScanner/src/main/res/values/ids.xml deleted file mode 100644 index 5f7ceae156..0000000000 --- a/libs/DocumentScanner/src/main/res/values/ids.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values/strings.xml b/libs/DocumentScanner/src/main/res/values/strings.xml deleted file mode 100644 index 1a6465b0bd..0000000000 --- a/libs/DocumentScanner/src/main/res/values/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - Cancel - Auto - Manual - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values/styles.xml b/libs/DocumentScanner/src/main/res/values/styles.xml deleted file mode 100644 index 6070315a53..0000000000 --- a/libs/DocumentScanner/src/main/res/values/styles.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/libs/annotations/build.gradle b/libs/annotations/build.gradle index 072df18231..9fbeaa9100 100644 --- a/libs/annotations/build.gradle +++ b/libs/annotations/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' static String isTesting() { if ( System.getenv("IS_TESTING") == "true" ) { @@ -96,7 +96,7 @@ repositories { username pspdfMavenUser password pspdfMavenPass } - url 'https://customers.pspdfkit.com/maven/' + url 'https://my.nutrient.io/maven' } } @@ -105,7 +105,7 @@ dependencies { api project(path: ':pandautils') api project(path: ':pandares') - api Libs.PSPDFKIT + api Libs.NUTRIENT androidTestImplementation Libs.JUNIT testImplementation Libs.JUNIT diff --git a/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt b/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt index 99fabce95c..18774af417 100644 --- a/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt +++ b/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt @@ -115,7 +115,7 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation .setAnnotationInspectorEnabled(true) .layoutMode(PageLayoutMode.SINGLE) .textSelectionEnabled(false) - .disableCopyPaste() + .copyPastEnabled(false) .build() private val annotationCreationToolbar = AnnotationCreationToolbar(context) @@ -174,9 +174,8 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation protected fun unregisterPdfFragmentListeners() { pdfFragment?.removeOnAnnotationCreationModeChangeListener(this) pdfFragment?.removeOnAnnotationEditingModeChangeListener(this) - pdfFragment?.document?.annotationProvider?.removeOnAnnotationUpdatedListener(annotationUpdateListener) + pdfFragment?.removeOnAnnotationUpdatedListener(annotationUpdateListener) pdfFragment?.removeOnAnnotationSelectedListener(annotationSelectedListener) - pdfFragment?.removeOnAnnotationDeselectedListener(annotationDeselectedListener) } /** @@ -198,12 +197,12 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation if (toolbar is AnnotationCreationToolbar) { setUpGrabAnnotationTool(toolbar) + toolbar.setMenuItemGroupingRule(AnnotationCreationGroupingRule(context)) } } }) annotationCreationToolbar.closeButton.setGone() - annotationCreationToolbar.setMenuItemGroupingRule(AnnotationCreationGroupingRule(context)) annotationEditingToolbar.setMenuItemGroupingRule(object : MenuItemGroupingRule { override fun groupMenuItems(items: MutableList, i: Int) = configureEditMenuItemGrouping(items) @@ -415,9 +414,6 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation annotationsJob = tryWeave { // Snag them annotations with the session id val annotations = awaitApi { CanvaDocsManager.getAnnotations(apiValues.sessionId, apiValues.canvaDocsDomain, it) } - // We don't want to trigger the annotation events here, so unregister and re-register after - pdfFragment?.document?.annotationProvider?.removeOnAnnotationUpdatedListener(annotationUpdateListener) - // Grab all the annotations and sort them by type (descending). // This will result in all of the comments being iterated over first as the COMMENT_REPLY type is last in the AnnotationType enum. val sortedAnnotationList = annotations.data.sortedByDescending { it.annotationType } @@ -456,9 +452,8 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation } noteHinter?.notifyDrawablesChanged() - pdfFragment?.document?.annotationProvider?.addOnAnnotationUpdatedListener(annotationUpdateListener) + pdfFragment?.addOnAnnotationUpdatedListener(annotationUpdateListener) pdfFragment?.addOnAnnotationSelectedListener(annotationSelectedListener) - pdfFragment?.addOnAnnotationDeselectedListener(annotationDeselectedListener) } catch { // Show error toast(R.string.annotationErrorOccurred) @@ -618,10 +613,6 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation && commentRepliesHashMap[currentAnnotation.annotationId]?.isNotEmpty() == true } - private val annotationDeselectedListener = AnnotationManager.OnAnnotationDeselectedListener { _, _ -> - commentsButton.setGone() - } - //region Annotation Manipulation fun createNewAnnotation(annotation: Annotation) { if (annotation.type == AnnotationType.FREETEXT) { @@ -642,9 +633,7 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation // Edit the annotation with the appropriate id annotation.name = newAnnotation.annotationId - pdfFragment?.document?.annotationProvider?.removeOnAnnotationUpdatedListener(annotationUpdateListener) pdfFragment?.notifyAnnotationHasChanged(annotation) - pdfFragment?.document?.annotationProvider?.addOnAnnotationUpdatedListener(annotationUpdateListener) commentsButton.isEnabled = true if (annotation.type == AnnotationType.STAMP) { commentsButton.setVisible() @@ -1017,7 +1006,7 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation // If the user has read/write/manage we want to let them delete (and only delete) non-authored annotations val annotation = pdfFragment?.selectedAnnotations?.get(0) - if (docSession.annotationMetadata?.canManage() == true && annotation?.flags?.contains(AnnotationFlags.LOCKED) == true) { + if (::docSession.isInitialized && docSession.annotationMetadata?.canManage() == true && annotation?.flags?.contains(AnnotationFlags.LOCKED) == true) { // We need to only return a list with the delete menu item delete = ContextualToolbarMenuItem.createSingleItem(context, View.generateViewId(), ContextCompat.getDrawable(context, R.drawable.ic_trash)!!, diff --git a/libs/canvas-api-2/build.gradle b/libs/canvas-api-2/build.gradle index 7f69f3ed91..a94afaacf2 100644 --- a/libs/canvas-api-2/build.gradle +++ b/libs/canvas-api-2/build.gradle @@ -19,7 +19,7 @@ apply plugin: 'com.android.library' apply plugin: 'com.apollographql.apollo' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' def pineDebugBaseUrl = "https://pine-api-dev.domain-svcs.nonprod.inseng.io" @@ -202,10 +202,10 @@ dependencies { implementation Libs.FIREBASE_CONFIG implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER implementation Libs.ROOM - kapt Libs.ROOM_COMPILER + ksp Libs.ROOM_COMPILER implementation Libs.PENDO diff --git a/libs/horizon/build.gradle.kts b/libs/horizon/build.gradle.kts index b10db9822a..41d8a6056d 100644 --- a/libs/horizon/build.gradle.kts +++ b/libs/horizon/build.gradle.kts @@ -3,7 +3,7 @@ plugins { id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.plugin.compose") id("kotlin-android") - id("kotlin-kapt") + id("com.google.devtools.ksp") id("dagger.hilt.android.plugin") kotlin("plugin.serialization") version "2.1.20" id("jacoco") @@ -16,7 +16,6 @@ android { defaultConfig { minSdk = Versions.MIN_SDK - targetSdk = Versions.TARGET_SDK testInstrumentationRunner = "com.instructure.horizon.espresso.HorizonCustomTestRunner" consumerProguardFiles("consumer-rules.pro") @@ -69,11 +68,11 @@ dependencies { implementation(Libs.NAVIGATION_COMPOSE) implementation(Libs.HILT) - kapt(Libs.HILT_COMPILER) + ksp(Libs.HILT_COMPILER) implementation(Libs.HILT_ANDROIDX_WORK) - kapt(Libs.HILT_ANDROIDX_COMPILER) + ksp(Libs.HILT_ANDROIDX_COMPILER) - implementation(Libs.PSPDFKIT) + implementation(Libs.NUTRIENT) implementation(Libs.ANDROIDX_ANNOTATION) implementation(Libs.ANDROIDX_APPCOMPAT) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/account/filepreview/PdfPreview.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/account/filepreview/PdfPreview.kt index 8e144427f0..fc3443b886 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/account/filepreview/PdfPreview.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/account/filepreview/PdfPreview.kt @@ -25,10 +25,8 @@ import com.pspdfkit.configuration.activity.UserInterfaceViewMode import com.pspdfkit.configuration.page.PageScrollDirection import com.pspdfkit.configuration.page.PageScrollMode import com.pspdfkit.jetpack.compose.interactors.rememberDocumentState -import com.pspdfkit.jetpack.compose.utilities.ExperimentalPSPDFKitApi import com.pspdfkit.jetpack.compose.views.DocumentView -@OptIn(ExperimentalPSPDFKitApi::class) @Composable fun PdfPreview( documentUri: Uri, diff --git a/libs/interactions/build.gradle b/libs/interactions/build.gradle index f54245df48..05450a79f4 100644 --- a/libs/interactions/build.gradle +++ b/libs/interactions/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'kotlin-parcelize' static String isTesting() { diff --git a/libs/login-api-2/build.gradle b/libs/login-api-2/build.gradle index 2a49d712e0..38a16be117 100644 --- a/libs/login-api-2/build.gradle +++ b/libs/login-api-2/build.gradle @@ -20,7 +20,7 @@ import java.security.MessageDigest apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' static String isTesting() { @@ -134,7 +134,7 @@ dependencies { implementation Libs.ANDROIDX_FRAGMENT_KTX implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER } task copySnickerDoodles(type: Copy) { diff --git a/libs/pandautils/build.gradle b/libs/pandautils/build.gradle index 9cce7f814b..345053752e 100644 --- a/libs/pandautils/build.gradle +++ b/libs/pandautils/build.gradle @@ -19,6 +19,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'org.jetbrains.kotlin.plugin.compose' @@ -48,6 +49,9 @@ android { ) } } + ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) + } } buildTypes { @@ -116,6 +120,7 @@ configurations { androidTestImplementation.exclude module:'protobuf-lite' all*.resolutionStrategy { + force 'io.grpc:grpc-netty:1.75.0' force Libs.KOTLIN_STD_LIB } } @@ -132,6 +137,9 @@ dependencies { implementation Libs.LOTTIE + // Explicitly add secure grpc-netty version to override any transitive dependencies + implementation 'io.grpc:grpc-netty:1.75.0' + /* Kotlin */ implementation Libs.KOTLIN_STD_LIB implementation Libs.ANDROIDX_BROWSER @@ -158,7 +166,7 @@ dependencies { api (Libs.GLIDE_OKHTTP) { exclude group: "com.android.support" } - kapt Libs.GLIDE_COMPILER + ksp Libs.GLIDE_COMPILER api Libs.ANDROID_SVG @@ -197,9 +205,9 @@ dependencies { /* DI */ implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER implementation Libs.HILT_ANDROIDX_WORK - kapt Libs.HILT_ANDROIDX_COMPILER + ksp Libs.HILT_ANDROIDX_COMPILER /* AAC */ implementation Libs.VIEW_MODEL @@ -210,7 +218,7 @@ dependencies { /* ROOM */ implementation Libs.ROOM - kapt Libs.ROOM_COMPILER + ksp Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES /* Compose */ @@ -245,8 +253,6 @@ dependencies { // More details here: classpath https://github.com/google/ExoPlayer/issues/7905 implementation 'com.google.guava:guava:29.0-android' - kaptTest Libs.ANDROIDX_DATABINDING_COMPILER - androidTestImplementation Libs.KOTLIN_COROUTINES_TEST androidTestImplementation (project(':espresso')) { exclude group: 'org.checkerframework', module: 'checker' diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt index 4aed3d92a7..56ee96a137 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt @@ -72,7 +72,20 @@ class SettingsScreenTest { } items.forEach { (title, items) -> - composeTestRule.onNodeWithText(context.getString(title)).assertExists() + retry(catchBlock = { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val y = device.displayHeight / 2 + val x = device.displayWidth / 2 + device.swipe( + x, + y, + x, + 0, + 10 + ) + }) { + composeTestRule.onNodeWithText(context.getString(title)).assertExists() + } items.forEach { item -> retry(catchBlock = { val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDaoTest.kt index 3b786f8448..1badd32d6e 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDaoTest.kt @@ -61,15 +61,13 @@ class GradingPeriodDaoTest { Assert.assertEquals(gradingPeriodEntity, result) } - @Test - fun testFindEntityByIdReturnsNullIfNotFound() = runTest { + @Test(expected = IllegalStateException::class) + fun testFindEntityByIdThrowsExceptionIfNotFound() = runTest { val gradingPeriodEntity = GradingPeriodEntity(GradingPeriod(id = 1, "Grading period 1")) val gradingPeriodEntity2 = GradingPeriodEntity(GradingPeriod(id = 2, "Grading period 2")) gradingPeriodDao.insert(gradingPeriodEntity) gradingPeriodDao.insert(gradingPeriodEntity2) val result = gradingPeriodDao.findById(3) - - Assert.assertNull(result) } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/TriStateBottomSheet.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/TriStateBottomSheet.kt index 63086fa181..fbabb10864 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/TriStateBottomSheet.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/TriStateBottomSheet.kt @@ -135,8 +135,8 @@ fun TriStateBottomSheet( anchoredDraggableState.updateAnchors(newAnchors) val currentTarget = anchoredDraggableState.targetValue - if (!newAnchors.hasAnchorFor(currentTarget)) { - if (newAnchors.hasAnchorFor(initialAnchor)) { + if (!newAnchors.hasPositionFor(currentTarget)) { + if (newAnchors.hasPositionFor(initialAnchor)) { anchoredDraggableState.snapTo(initialAnchor) } else if (newAnchors.size > 0) { newAnchors.closestAnchor(anchoredDraggableState.offset.takeIf { !it.isNaN() } @@ -153,7 +153,7 @@ fun TriStateBottomSheet( LaunchedEffect(anchoredDraggableState.anchors, initialAnchor) { val currentAnchors = anchoredDraggableState.anchors - if (currentAnchors.hasAnchorFor(initialAnchor) && anchoredDraggableState.currentValue != initialAnchor) { + if (currentAnchors.hasPositionFor(initialAnchor) && anchoredDraggableState.currentValue != initialAnchor) { if (anchoredDraggableState.targetValue != initialAnchor || !anchoredDraggableState.isAnimationRunning) { anchoredDraggableState.snapTo(initialAnchor) } @@ -168,8 +168,8 @@ fun TriStateBottomSheet( if (offset.isNaN() || currentAnchors.size == 0) { getPixelForAnchor(initialAnchor) } else { - val minAnchorValue = currentAnchors.minAnchor() - val maxAnchorValue = currentAnchors.maxAnchor() + val minAnchorValue = currentAnchors.minPosition() + val maxAnchorValue = currentAnchors.maxPosition() offset.coerceIn(minAnchorValue, maxAnchorValue) } } @@ -259,7 +259,7 @@ fun TriStateBottomSheet( coroutineScope.launch { when (anchoredDraggableState.targetValue) { AnchorPoints.BOTTOM -> { - if (anchoredDraggableState.anchors.hasAnchorFor(AnchorPoints.MIDDLE)) { + if (anchoredDraggableState.anchors.hasPositionFor(AnchorPoints.MIDDLE)) { anchoredDraggableState.animateTo(AnchorPoints.MIDDLE) } else { anchoredDraggableState.animateTo(AnchorPoints.BOTTOM) @@ -271,7 +271,7 @@ fun TriStateBottomSheet( } AnchorPoints.TOP -> { - if (anchoredDraggableState.anchors.hasAnchorFor(AnchorPoints.MIDDLE)) { + if (anchoredDraggableState.anchors.hasPositionFor(AnchorPoints.MIDDLE)) { anchoredDraggableState.animateTo(AnchorPoints.MIDDLE) } else { anchoredDraggableState.animateTo(AnchorPoints.BOTTOM) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt index 028f55bfbb..de52b82926 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt @@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo import androidx.work.WorkManager -import androidx.work.await import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.managers.AccountNotificationManager import com.instructure.canvasapi2.managers.ConferenceManager @@ -64,6 +63,7 @@ import com.instructure.pandautils.room.offline.daos.StudioMediaProgressDao import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.threeten.bp.OffsetDateTime import java.util.Locale @@ -292,36 +292,33 @@ class DashboardNotificationsViewModel @Inject constructor( private suspend fun getUploads(fileUploadEntities: List?) = fileUploadEntities?.mapNotNull { fileUploadEntity -> val workerId = UUID.fromString(fileUploadEntity.workerId) - workManager.getWorkInfoById(workerId).await()?.let { workInfo -> + val workInfo = workManager.getWorkInfoByIdFlow(workerId).first() + workInfo?.let { val icon: Int val background: Int - when (workInfo.state) { + when (it.state) { WorkInfo.State.FAILED -> { icon = R.drawable.ic_exclamation_mark background = R.color.backgroundDanger } - WorkInfo.State.SUCCEEDED -> { icon = R.drawable.ic_check_white_24dp background = R.color.backgroundSuccess } - else -> { icon = R.drawable.ic_upload background = R.color.backgroundInfo } } - val uploadViewData = UploadViewData( fileUploadEntity.title.orEmpty(), fileUploadEntity.subtitle.orEmpty(), - icon, background, workInfo.state == WorkInfo.State.RUNNING + icon, background, it.state == WorkInfo.State.RUNNING ) - UploadItemViewModel( workerId = workerId, workManager = workManager, data = uploadViewData, - open = { uuid -> openUploadNotification(workInfo.state, uuid, fileUploadEntity) }, + open = { uuid -> openUploadNotification(it.state, uuid, fileUploadEntity) }, remove = { removeUploadNotification(fileUploadEntity, workerId) } ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt index f0d2e65c20..48d45185c7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt @@ -19,8 +19,6 @@ package com.instructure.pandautils.features.dashboard.notifications.itemviewmode import androidx.databinding.BaseObservable import androidx.databinding.Bindable -import androidx.lifecycle.Observer -import androidx.work.WorkInfo import androidx.work.WorkManager import com.instructure.pandautils.BR import com.instructure.pandautils.R @@ -28,7 +26,12 @@ import com.instructure.pandautils.features.dashboard.notifications.UploadViewDat import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker.Companion.PROGRESS_DATA_FULL_SIZE import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker.Companion.PROGRESS_DATA_UPLOADED_SIZE import com.instructure.pandautils.mvvm.ItemViewModel -import java.util.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.util.UUID class UploadItemViewModel( val workerId: UUID, @@ -39,23 +42,25 @@ class UploadItemViewModel( val remove: () -> Unit, @get:Bindable var loading: Boolean = false ) : ItemViewModel, BaseObservable() { - override val layoutId = R.layout.item_dashboard_upload - - private val observer = Observer { - val uploadedSize = it.progress.getLong(PROGRESS_DATA_UPLOADED_SIZE, 0L) - val fullSize = it.progress.getLong(PROGRESS_DATA_FULL_SIZE, 1L) - - progress = ((uploadedSize.toDouble() / fullSize.toDouble()) * 100.0).toInt() - notifyPropertyChanged(BR.progress) - } + private var job: Job? = null init { - workManager.getWorkInfoByIdLiveData(workerId).observeForever(observer) + job = CoroutineScope(Dispatchers.Main).launch { + workManager.getWorkInfoByIdFlow(workerId).collectLatest { workInfo -> + workInfo?.let { + val uploadedSize = it.progress.getLong(PROGRESS_DATA_UPLOADED_SIZE, 0L) + val fullSize = it.progress.getLong(PROGRESS_DATA_FULL_SIZE, 1L) + + progress = ((uploadedSize.toDouble() / fullSize.toDouble()) * 100.0).toInt() + notifyPropertyChanged(BR.progress) + } + } + } } fun clear() { - workManager.getWorkInfoByIdLiveData(workerId).removeObserver(observer) + job?.cancel() } fun open() = open.invoke(workerId) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt index 9a835b82fd..82072ba28a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt @@ -248,7 +248,7 @@ class FileUploadDialogFragment : BaseCanvasDialogFragment() { } is FileUploadAction.UploadStartedAction -> { getParent()?.selectedUriStringsCallback(action.selectedUris) - getParent()?.workInfoLiveDataCallback(action.id, action.liveData) + getParent()?.workInfoLiveDataCallback(action.id,action.liveData) lifecycleScope.launch { fileUploadEventHandler.postEvent( FileUploadEvent.FileSelected(action.selectedUris) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogParent.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogParent.kt index 11d4099ef7..4a16d76981 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogParent.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogParent.kt @@ -28,5 +28,5 @@ interface FileUploadDialogParent { fun selectedUriStringsCallback(filePaths: List) = Unit - fun workInfoLiveDataCallback(uuid: UUID? = null, workInfoLiveData: LiveData) = Unit + fun workInfoLiveDataCallback(uuid: UUID? = null, workInfoLiveData: LiveData) = Unit } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt index bc47a0eaf8..185b8efb7e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt @@ -42,6 +42,6 @@ sealed class FileUploadAction { object UploadStarted : FileUploadAction() data class ShowToast(val toast: String) : FileUploadAction() data class AttachmentSelectedAction(val event: Int, val attachment: FileSubmitObject?) : FileUploadAction() - data class UploadStartedAction(val id: UUID, val liveData: LiveData, val selectedUris: List) : FileUploadAction() + data class UploadStartedAction(val id: UUID, val liveData: LiveData, val selectedUris: List) : FileUploadAction() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt index 225cb4f9fc..9510bbc275 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt @@ -29,7 +29,7 @@ sealed class FileUploadEvent { data class FileSelected(val filePaths: List) : FileUploadEvent() data class UploadStarted( val uuid: UUID?, - val workInfoLiveData: LiveData, + val workInfoLiveData: LiveData, val filePaths: List ) : FileUploadEvent() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt index 1d6f2c845d..0a7d5accce 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt @@ -331,7 +331,7 @@ class FileUploadWorker @AssistedInject constructor( } }).dataOrThrow - val updatedList = workDataBuilder.build() + val updatedList: Array = workDataBuilder.build() .getStringArray(PROGRESS_DATA_UPLOADED_FILES) .orEmpty() .toMutableList() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt index c4ab26e01f..ead5c82dd7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt @@ -82,7 +82,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @@ -291,7 +291,7 @@ private fun GradesScreenContent( modifier = Modifier .height(24.dp) .semantics { - invisibleToUser() + hideFromAccessibility() } ) } @@ -301,19 +301,19 @@ private fun GradesScreenContent( } } - uiState.items.forEach { + uiState.items.forEach { item -> stickyHeader { GroupHeader( - name = it.name, - expanded = it.expanded, + name = item.name, + expanded = item.expanded, onClick = { - actionHandler(GradesAction.GroupHeaderClick(it.id)) + actionHandler(GradesAction.GroupHeaderClick(item.id)) } ) } - if (it.expanded) { - items(it.assignments) { assignment -> + if (item.expanded) { + items(item.assignments) { assignment -> AssignmentItem(assignment, actionHandler, userColor) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt index 74f3227384..c8097976b8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt @@ -111,9 +111,11 @@ class InboxComposeFragment : BaseCanvasFragment(), FragmentInteractions, FileUpl viewModel.addUploadingAttachments(filePaths) } - override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { + override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { workInfoLiveData.observe(viewLifecycleOwner) { workInfo -> - viewModel.updateAttachments(uuid, workInfo) + workInfo?.let { + viewModel.updateAttachments(uuid, workInfo) + } } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelper.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelper.kt index 920c6f0367..f97088c9a1 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelper.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelper.kt @@ -25,10 +25,10 @@ import androidx.work.OneTimeWorkRequest import androidx.work.PeriodicWorkRequest import androidx.work.WorkInfo import androidx.work.WorkManager -import androidx.work.await import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.offline.sync.settings.SyncFrequency import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade +import kotlinx.coroutines.flow.first import java.util.concurrent.TimeUnit class OfflineSyncHelper( @@ -103,17 +103,16 @@ class OfflineSyncHelper( } private suspend fun isWorkScheduled(): Boolean { - return workManager.getWorkInfosForUniqueWork(apiPrefs.user?.id.toString()).await() - .any { it.state != WorkInfo.State.CANCELLED } + return workManager.getWorkInfosForUniqueWorkFlow(apiPrefs.user?.id.toString()).first().any { it.state != WorkInfo.State.CANCELLED } } private suspend fun isPeriodicWorkRunning(): Boolean { - return workManager.getWorkInfosForUniqueWork(apiPrefs.user?.id.toString()).await() + return workManager.getWorkInfosForUniqueWorkFlow(apiPrefs.user?.id.toString()).first() .any { it.state == WorkInfo.State.RUNNING } } private suspend fun getRunningOneTimeWorkInfo(): WorkInfo? { - return workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG).await() + return workManager.getWorkInfosByTagFlow(OfflineSyncWorker.ONE_TIME_TAG).first() .firstOrNull { it.state == WorkInfo.State.RUNNING } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt index 058d8d1caf..26c5e20609 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt @@ -170,7 +170,7 @@ abstract class ShareExtensionActivity : BaseCanvasActivity(), FileUploadDialogPa } } - override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { + override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { uuid?.let { shareExtensionViewModel.workerCallback(it) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt index c88be6a8f3..7bb6fc7ec3 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt @@ -5,7 +5,6 @@ import android.net.Uri import androidx.annotation.DrawableRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.* @@ -24,7 +23,9 @@ import com.instructure.pandautils.utils.fromJson import com.instructure.pandautils.utils.humanReadableByteCount import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.collectLatest import java.io.File import java.util.* import javax.inject.Inject @@ -56,31 +57,35 @@ class ShareExtensionProgressDialogViewModel @Inject constructor( private var workerId: UUID? = null private var fileUploadType = FileUploadType.USER - - private val observer = Observer { - when (it.state) { - WorkInfo.State.SUCCEEDED -> { - _events.postValue(Event((ShareExtensionProgressAction.ShowSuccessDialog(fileUploadType)))) - } - WorkInfo.State.RUNNING -> { - viewModelScope.launch { updateViewData(it.progress, false) } - } - WorkInfo.State.FAILED -> { - viewModelScope.launch { updateViewData(it.outputData, true) } - } - else -> {} - } - } + private var job: Job? = null fun setUUID(uuid: UUID) { this.workerId = uuid _state.postValue(ViewState.Loading) - workManager.getWorkInfoByIdLiveData(uuid).observeForever(observer) + job?.cancel() + job = viewModelScope.launch { + workManager.getWorkInfoByIdFlow(uuid).collectLatest { it -> + it?.let { + when (it.state) { + WorkInfo.State.SUCCEEDED -> { + _events.postValue(Event((ShareExtensionProgressAction.ShowSuccessDialog(fileUploadType)))) + } + WorkInfo.State.RUNNING -> { + updateViewData(it.progress, false) + } + WorkInfo.State.FAILED -> { + updateViewData(it.outputData, true) + } + else -> {} + } + } + } + } } override fun onCleared() { super.onCleared() - workerId?.let { workManager.getWorkInfoByIdLiveData(it).removeObserver(observer) } + job?.cancel() } private suspend fun updateViewData(progress: Data, failed: Boolean) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsUiState.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsUiState.kt index 416a4b6e82..a0c0895198 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsUiState.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsUiState.kt @@ -77,6 +77,6 @@ sealed class SpeedGraderCommentsAction { data object ChooseFilesClicked : SpeedGraderCommentsAction() data object FileUploadDialogClosed : SpeedGraderCommentsAction() data class FilesSelected(val filePaths: List) : SpeedGraderCommentsAction() - data class FileUploadStarted(val workInfoLiveData: LiveData) : SpeedGraderCommentsAction() + data class FileUploadStarted(val workInfoLiveData: LiveData) : SpeedGraderCommentsAction() data class MediaRecorded(val file: File) : SpeedGraderCommentsAction() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt index 891080153a..e45765c333 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/SpeedGraderCommentsViewModel.kt @@ -356,7 +356,7 @@ class SpeedGraderCommentsViewModel @Inject constructor( } } - private fun onFileUploadStarted(workInfoLiveData: LiveData, filePaths: List) { + private fun onFileUploadStarted(workInfoLiveData: LiveData, filePaths: List) { _uiState.update { state -> state.copy( fileSelectorDialogData = null, @@ -480,7 +480,7 @@ class SpeedGraderCommentsViewModel @Inject constructor( attemptId = selectedAttemptId, mediaCommentId = id ).collect { result -> - when (result.state) { + when (result?.state) { WorkInfo.State.SUCCEEDED -> { fetchedComments.add( SpeedGraderComment( diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/commentlibrary/SpeedGraderCommentLibraryScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/commentlibrary/SpeedGraderCommentLibraryScreen.kt index fc067d1739..0827475a76 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/commentlibrary/SpeedGraderCommentLibraryScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/speedgrader/grade/comments/commentlibrary/SpeedGraderCommentLibraryScreen.kt @@ -164,6 +164,7 @@ private fun SpeedGraderCommentLibraryContent( uiState.onCommentValueChanged(item) } .padding(vertical = 14.dp) + .testTag("commentLibraryItem") ) if (index != uiState.items.lastIndex) { CanvasDivider() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicEntity.kt index 32430df43e..f083aaa169 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicEntity.kt @@ -10,7 +10,7 @@ import com.instructure.canvasapi2.models.DiscussionTopic data class DiscussionTopicEntity( @PrimaryKey val id: Long, - val unreadEntries: MutableList, + val unreadEntries: List, val participantIds: List, val viewIds: List, ) { @@ -23,7 +23,7 @@ data class DiscussionTopicEntity( fun toApiModel(participants: List, views: List): DiscussionTopic { return DiscussionTopic( - unreadEntries = unreadEntries, + unreadEntries = unreadEntries.toMutableList(), participants = participants, unreadEntriesMap = hashMapOf(), entryRatings = hashMapOf(), diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/services/NotoriousUploadWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/services/NotoriousUploadWorker.kt index 3d09f4304c..5b5d748eba 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/services/NotoriousUploadWorker.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/services/NotoriousUploadWorker.kt @@ -284,7 +284,7 @@ class NotoriousUploadWorker @AssistedInject constructor( pageId: String?, attemptId: Long?, mediaCommentId: Long? - ): Flow { + ): Flow { val data = workDataOf( Const.MEDIA_FILE_PATH to mediaFilePath, Const.ASSIGNMENT to assignment?.toJson(), diff --git a/libs/pandautils/src/main/res/layout/view_canvas_web_view_wrapper.xml b/libs/pandautils/src/main/res/layout/view_canvas_web_view_wrapper.xml index 7b658476ef..301bb65318 100644 --- a/libs/pandautils/src/main/res/layout/view_canvas_web_view_wrapper.xml +++ b/libs/pandautils/src/main/res/layout/view_canvas_web_view_wrapper.xml @@ -35,7 +35,7 @@ android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:background="@drawable/bg_button_full_rounded" - android:foreground="?selectableItemBackground" + android:foreground="?android:attr/selectableItemBackground" android:gravity="center" android:minHeight="48dp" android:orientation="horizontal" diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt index 933fe7d9ea..e1e5f32f13 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt @@ -23,11 +23,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.observe import androidx.work.Data import androidx.work.WorkInfo import androidx.work.WorkManager -import com.google.common.util.concurrent.Futures import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.managers.AccountNotificationManager import com.instructure.canvasapi2.managers.ConferenceManager @@ -73,6 +71,7 @@ import junit.framework.Assert.assertNotNull import junit.framework.Assert.assertNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.setMain import org.junit.After @@ -504,7 +503,7 @@ class DashboardNotificationsViewModelTest { DashboardFileUploadEntity(workerId3.toString(), 1, title3, subTitle3, null, null, null, null) ) - every { workManager.getWorkInfoById(workerId) } returns Futures.immediateFuture( + every { workManager.getWorkInfoByIdFlow(workerId) } returns flowOf( WorkInfo( workerId, WorkInfo.State.RUNNING, @@ -521,7 +520,7 @@ class DashboardNotificationsViewModelTest { ) ) - every { workManager.getWorkInfoById(workerId2) } returns Futures.immediateFuture( + every { workManager.getWorkInfoByIdFlow(workerId2) } returns flowOf( WorkInfo( workerId2, WorkInfo.State.SUCCEEDED, @@ -538,7 +537,7 @@ class DashboardNotificationsViewModelTest { ) ) - every { workManager.getWorkInfoById(workerId3) } returns Futures.immediateFuture( + every { workManager.getWorkInfoByIdFlow(workerId3) } returns flowOf( WorkInfo( workerId3, WorkInfo.State.FAILED, @@ -585,7 +584,7 @@ class DashboardNotificationsViewModelTest { DashboardFileUploadEntity(workerId.toString(), 1, title, subTitle, null, null, null, null) ) - every { workManager.getWorkInfoById(workerId) } returns Futures.immediateFuture( + every { workManager.getWorkInfoByIdFlow(workerId) } returns flowOf( WorkInfo( workerId, WorkInfo.State.RUNNING, @@ -602,7 +601,7 @@ class DashboardNotificationsViewModelTest { assertEquals(1, viewModel.data.value?.uploadItems?.size) assertEquals(expectedRunning, viewModel.data.value?.uploadItems?.first()?.data) - every { workManager.getWorkInfoById(workerId) } returns Futures.immediateFuture( + every { workManager.getWorkInfoByIdFlow(workerId) } returns flowOf( WorkInfo( workerId, WorkInfo.State.SUCCEEDED, @@ -630,7 +629,7 @@ class DashboardNotificationsViewModelTest { DashboardFileUploadEntity(workerId.toString(), 1, "", "", null, null, null, null) ) - every { workManager.getWorkInfoById(workerId) } returns Futures.immediateFuture( + every { workManager.getWorkInfoByIdFlow(workerId) } returns flowOf( WorkInfo( workerId, WorkInfo.State.RUNNING, @@ -665,7 +664,7 @@ class DashboardNotificationsViewModelTest { DashboardFileUploadEntity(workerId.toString(), 1, "", "", 1, 2, 3, null) ) - every { workManager.getWorkInfoById(workerId) } returns Futures.immediateFuture( + every { workManager.getWorkInfoByIdFlow(workerId) } returns flowOf( WorkInfo( workerId, WorkInfo.State.SUCCEEDED, @@ -706,7 +705,7 @@ class DashboardNotificationsViewModelTest { DashboardFileUploadEntity(workerId.toString(), 1, "", "", null, null, null, 0) ) - every { workManager.getWorkInfoById(workerId) } returns Futures.immediateFuture( + every { workManager.getWorkInfoByIdFlow(workerId) } returns flowOf( WorkInfo( workerId, WorkInfo.State.SUCCEEDED, diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt index f4bd92e64e..3ea7820f13 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt @@ -29,8 +29,6 @@ import androidx.work.PeriodicWorkRequest import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkRequest -import androidx.work.impl.OperationImpl -import com.google.common.util.concurrent.Futures import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.offline.sync.settings.SyncFrequency @@ -45,6 +43,7 @@ import io.mockk.verify import junit.framework.TestCase.assertEquals import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain @@ -77,8 +76,10 @@ class OfflineSyncHelperTest { every { apiPrefs.user } returns User(1L) - every { workManager.enqueue(any()) } returns OperationImpl() - every { workManager.enqueueUniquePeriodicWork(any(), any(), any()) } returns OperationImpl() + every { workManager.enqueue(any()) } returns mockk() + every { workManager.enqueueUniquePeriodicWork(any(), any(), any()) } returns mockk() + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf(emptyList()) + every { workManager.getWorkInfosByTagFlow(any()) } returns flowOf(emptyList()) coEvery { syncSettingsFacade.getSyncSettings() } returns SyncSettingsEntity( 1L, @@ -100,8 +101,8 @@ class OfflineSyncHelperTest { SyncFrequency.DAILY, true ) - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(mockk(relaxed = true)) - every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf(listOf(mockk(relaxed = true))) + every { workManager.getWorkInfosByTagFlow(OfflineSyncWorker.ONE_TIME_TAG) } returns flowOf( emptyList() ) @@ -116,7 +117,7 @@ class OfflineSyncHelperTest { val courseIds = listOf(1L, 2L, 3L) coEvery { syncSettingsFacade.getSyncSettings() } returns SyncSettingsEntity(1L, true, SyncFrequency.DAILY, true) - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(emptyList()) + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf(emptyList()) offlineSyncHelper.syncCourses(courseIds) @@ -134,8 +135,8 @@ class OfflineSyncHelperTest { SyncFrequency.DAILY, true ) - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(emptyList()) - every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf(emptyList()) + every { workManager.getWorkInfosByTagFlow(OfflineSyncWorker.ONE_TIME_TAG) } returns flowOf( emptyList() ) @@ -147,7 +148,7 @@ class OfflineSyncHelperTest { @Test fun `Cancel should cancel work with correct id`() { - every { workManager.cancelUniqueWork(any()) } returns OperationImpl() + every { workManager.cancelUniqueWork(any()) } returns mockk() offlineSyncHelper.cancelWork() @@ -164,8 +165,8 @@ class OfflineSyncHelperTest { SyncFrequency.DAILY, true ) - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(emptyList()) - every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf(emptyList()) + every { workManager.getWorkInfosByTagFlow(OfflineSyncWorker.ONE_TIME_TAG) } returns flowOf( emptyList() ) @@ -260,7 +261,7 @@ class OfflineSyncHelperTest { coVerify { workManager.enqueueUniquePeriodicWork(any(), any(), capture(originalCaptor)) } val originalRequest = originalCaptor.captured - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture( + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf( listOf( WorkInfo(originalRequest.id, WorkInfo.State.ENQUEUED, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1) ) @@ -278,8 +279,8 @@ class OfflineSyncHelperTest { @Test fun `Cancel running one time workers`() = runTest { val uuid = UUID.randomUUID() - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(emptyList()) - every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf(emptyList()) + every { workManager.getWorkInfosByTagFlow(OfflineSyncWorker.ONE_TIME_TAG) } returns flowOf( listOf( WorkInfo(uuid, WorkInfo.State.RUNNING, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1) ) @@ -295,12 +296,12 @@ class OfflineSyncHelperTest { @Test fun `Cancel running periodic workers`() = runTest { val uuid = UUID.randomUUID() - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture( + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf( listOf( WorkInfo(uuid, WorkInfo.State.RUNNING, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1) ) ) - every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + every { workManager.getWorkInfosByTagFlow(OfflineSyncWorker.ONE_TIME_TAG) } returns flowOf( emptyList() ) @@ -320,7 +321,7 @@ class OfflineSyncHelperTest { @Test fun `scheduleWorkAfterLogin should schedule work when auto sync is enabled and no work is already scheduled`() = runTest { - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(emptyList()) + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf(emptyList()) coEvery { syncSettingsFacade.getSyncSettings() } returns SyncSettingsEntity( autoSyncEnabled = true, syncFrequency = SyncFrequency.DAILY, @@ -348,12 +349,12 @@ class OfflineSyncHelperTest { @Test fun `Restart periodic worker`() = runTest { val uuid = UUID.randomUUID() - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture( + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf( listOf( WorkInfo(uuid, WorkInfo.State.RUNNING, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1) ) ) - every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + every { workManager.getWorkInfosByTagFlow(OfflineSyncWorker.ONE_TIME_TAG) } returns flowOf( emptyList() ) @@ -372,10 +373,10 @@ class OfflineSyncHelperTest { @Test fun `Restart one time worker`() = runTest { val uuid = UUID.randomUUID() - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture( + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf( emptyList() ) - every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + every { workManager.getWorkInfosByTagFlow(OfflineSyncWorker.ONE_TIME_TAG) } returns flowOf( listOf( WorkInfo(uuid, WorkInfo.State.RUNNING, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1) ) @@ -390,7 +391,7 @@ class OfflineSyncHelperTest { @Test fun `scheduleWorkAfterLogin should not schedule work when work is already scheduled`() = runTest { - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(mockk(relaxed = true)) + every { workManager.getWorkInfosForUniqueWorkFlow(any()) } returns flowOf(mockk(relaxed = true)) offlineSyncHelper.scheduleWorkAfterLogin() diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressViewModelTest.kt index bf6149478d..f334304ab0 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressViewModelTest.kt @@ -5,7 +5,6 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry -import androidx.lifecycle.MutableLiveData import androidx.work.Data import androidx.work.WorkInfo import androidx.work.WorkManager @@ -24,6 +23,7 @@ import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain @@ -51,7 +51,7 @@ class ShareExtensionProgressViewModelTest { private val dashboardFileUploadDao: DashboardFileUploadDao = mockk(relaxed = true) private val fileUploadUtilsHelper: FileUploadUtilsHelper = mockk(relaxed = true) - private lateinit var mockLiveData: MutableLiveData + private lateinit var mockFlow: MutableStateFlow private lateinit var viewModel: ShareExtensionProgressDialogViewModel private lateinit var uuid: UUID @@ -64,8 +64,8 @@ class ShareExtensionProgressViewModelTest { uuid = UUID.randomUUID() - mockLiveData = MutableLiveData() - every { workManager.getWorkInfoByIdLiveData(uuid) } returns mockLiveData + mockFlow = MutableStateFlow(null) + every { workManager.getWorkInfoByIdFlow(uuid) } returns mockFlow viewModel = createViewModel() } @@ -78,7 +78,7 @@ class ShareExtensionProgressViewModelTest { @Test fun `Show success dialog after uploading`() { viewModel.setUUID(uuid) - mockLiveData.postValue(WorkInfo(uuid, WorkInfo.State.SUCCEEDED, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1)) + mockFlow.value = WorkInfo(uuid, WorkInfo.State.SUCCEEDED, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1) viewModel.events.observe(lifecycleOwner) {} assertEquals(ShareExtensionProgressAction.ShowSuccessDialog(FileUploadType.USER), viewModel.events.value?.getContentIfNotHandled()) @@ -93,7 +93,7 @@ class ShareExtensionProgressViewModelTest { .putStringArray(FileUploadWorker.PROGRESS_DATA_FILES_TO_UPLOAD, emptyArray()) .build() - mockLiveData.postValue(WorkInfo(uuid, WorkInfo.State.FAILED, emptySet(), outputData, Data.EMPTY, 1, 1)) + mockFlow.value = WorkInfo(uuid, WorkInfo.State.FAILED, emptySet(), outputData, Data.EMPTY, 1, 1) assertEquals("Error", viewModel.data.value?.subtitle) } @@ -127,16 +127,14 @@ class ShareExtensionProgressViewModelTest { .putLong(FileUploadWorker.PROGRESS_DATA_FULL_SIZE, 1L) .putStringArray(FileUploadWorker.PROGRESS_DATA_FILES_TO_UPLOAD, emptyArray()) .build() - mockLiveData.postValue( - WorkInfo( - uuid, - WorkInfo.State.RUNNING, - emptySet(), - Data.EMPTY, - progressData, - 1, - 1 - ) + mockFlow.value = WorkInfo( + uuid, + WorkInfo.State.RUNNING, + emptySet(), + Data.EMPTY, + progressData, + 1, + 1 ) assertEquals(ViewState.Success, viewModel.state.value) @@ -147,7 +145,7 @@ class ShareExtensionProgressViewModelTest { every { resources.getString(R.string.submissionProgressSubtitle, "Assignment") } returns "Uploading submission to \"Assignment\"" - val filesToUpload = listOf( + val filesToUpload: Array = listOf( FileSubmitObject(name = "Test 1", size = 1L, contentType = "text/file", fullPath = ""), FileSubmitObject(name = "Test 2", size = 1L, contentType = "text/file", fullPath = "") ).map { it.toJson() }.toTypedArray() @@ -159,16 +157,14 @@ class ShareExtensionProgressViewModelTest { .putString(FileUploadWorker.PROGRESS_DATA_ASSIGNMENT_NAME, "Assignment") .build() - mockLiveData.postValue( - WorkInfo( - uuid, - WorkInfo.State.RUNNING, - emptySet(), - Data.EMPTY, - progressData, - 1, - 1 - ) + mockFlow.value = WorkInfo( + uuid, + WorkInfo.State.RUNNING, + emptySet(), + Data.EMPTY, + progressData, + 1, + 1 ) val expectedItemData = listOf( @@ -205,7 +201,7 @@ class ShareExtensionProgressViewModelTest { fun `Update view data when live data changes`() { every { resources.getString(R.string.submissionProgressSubtitle, "Assignment") } returns "Uploading submission to \"Assignment\"" - val filesToUpload = listOf( + val filesToUpload: Array = listOf( FileSubmitObject(name = "Test 1", size = 1L, contentType = "text/file", fullPath = ""), FileSubmitObject(name = "Test 2", size = 1L, contentType = "text/file", fullPath = "") ).map { it.toJson() }.toTypedArray() @@ -216,16 +212,14 @@ class ShareExtensionProgressViewModelTest { .putLong(FileUploadWorker.PROGRESS_DATA_UPLOADED_SIZE, 0L) .putString(FileUploadWorker.PROGRESS_DATA_ASSIGNMENT_NAME, "Assignment") - mockLiveData.postValue( - WorkInfo( - uuid, - WorkInfo.State.RUNNING, - emptySet(), - Data.EMPTY, - progressData.build(), - 1, - 1 - ) + mockFlow.value = WorkInfo( + uuid, + WorkInfo.State.RUNNING, + emptySet(), + Data.EMPTY, + progressData.build(), + 1, + 1 ) val expectedItemData = listOf( @@ -251,16 +245,14 @@ class ShareExtensionProgressViewModelTest { .putStringArray(FileUploadWorker.PROGRESS_DATA_UPLOADED_FILES, arrayOf(filesToUpload[0])) .putLong(FileUploadWorker.PROGRESS_DATA_UPLOADED_SIZE, 1L) - mockLiveData.postValue( - WorkInfo( - uuid, - WorkInfo.State.RUNNING, - emptySet(), - Data.EMPTY, - progressData.build(), - 1, - 1 - ) + mockFlow.value = WorkInfo( + uuid, + WorkInfo.State.RUNNING, + emptySet(), + Data.EMPTY, + progressData.build(), + 1, + 1 ) val viewData = viewModel.data.value @@ -277,12 +269,12 @@ class ShareExtensionProgressViewModelTest { @Test fun `Failed upload maps correctly`() { - val filesToUpload = listOf( + val filesToUpload: Array = listOf( FileSubmitObject(name = "Test 1", size = 1L, contentType = "text/file", fullPath = ""), FileSubmitObject(name = "Test 2", size = 1L, contentType = "text/file", fullPath = "") ).map { it.toJson() }.toTypedArray() - val uploadedFiles = listOf( + val uploadedFiles: Array = listOf( FileSubmitObject(name = "Test 1", size = 1L, contentType = "text/file", fullPath = "") ).map { it.toJson() }.toTypedArray() @@ -293,16 +285,14 @@ class ShareExtensionProgressViewModelTest { .putString(FileUploadWorker.PROGRESS_DATA_ASSIGNMENT_NAME, "Assignment") .build() - mockLiveData.postValue( - WorkInfo( - uuid, - WorkInfo.State.FAILED, - emptySet(), - progressData, - Data.EMPTY, - 1, - 1 - ) + mockFlow.value = WorkInfo( + uuid, + WorkInfo.State.FAILED, + emptySet(), + progressData, + Data.EMPTY, + 1, + 1 ) val expectedItemData = listOf( @@ -336,7 +326,7 @@ class ShareExtensionProgressViewModelTest { @Test fun `Failed upload retry`() { - val filesToUpload = listOf( + val filesToUpload: Array = listOf( FileSubmitObject(name = "Test 1", size = 1L, contentType = "text/file", fullPath = "") ).map { it.toJson() }.toTypedArray() @@ -345,69 +335,65 @@ class ShareExtensionProgressViewModelTest { .putStringArray(FileUploadWorker.PROGRESS_DATA_FILES_TO_UPLOAD, filesToUpload) .build() - mockLiveData.postValue( - WorkInfo( - uuid, - WorkInfo.State.FAILED, - emptySet(), - failedOutputData, - Data.EMPTY, - 1, - 1 - ) + mockFlow.value = WorkInfo( + uuid, + WorkInfo.State.FAILED, + emptySet(), + failedOutputData, + Data.EMPTY, + 1, + 1 ) viewModel.setUUID(uuid) + viewModel.data.observe(lifecycleOwner) {} + viewModel.events.observe(lifecycleOwner) {} val viewData = viewModel.data.value assertEquals("File Upload", viewData?.dialogTitle) assertEquals("Error", viewData?.subtitle) assertEquals(true, viewData?.failed) - viewModel.onRetryClick() - coEvery { fileUploadInputDao.findByWorkerId(uuid.toString()) } returns FileUploadInputEntity( workerId = uuid.toString(), action = "", filePaths = emptyList() ) - every { workManager.getWorkInfoByIdLiveData(any()) } returns mockLiveData + every { workManager.getWorkInfoByIdFlow(any()) } returns mockFlow every { resources.getString(R.string.fileUploadProgressSubtitle) } returns "Uploading files" + viewModel.onRetryClick() + val successProgressData = Data.Builder() .putLong(FileUploadWorker.PROGRESS_DATA_FULL_SIZE, 1L) .putStringArray(FileUploadWorker.PROGRESS_DATA_FILES_TO_UPLOAD, filesToUpload) .putStringArray(FileUploadWorker.PROGRESS_DATA_UPLOADED_FILES, filesToUpload) .build() - mockLiveData.postValue( - WorkInfo( - uuid, - WorkInfo.State.RUNNING, - emptySet(), - Data.EMPTY, - successProgressData, - 1, - 1 - ) + mockFlow.value = WorkInfo( + uuid, + WorkInfo.State.RUNNING, + emptySet(), + Data.EMPTY, + successProgressData, + 1, + 1 ) val successViewData = viewModel.data.value assertEquals("Uploading files", successViewData?.subtitle) assertEquals(false, successViewData?.failed) - mockLiveData.postValue( - WorkInfo( - uuid, - WorkInfo.State.SUCCEEDED, - emptySet(), - Data.EMPTY, - successProgressData, - 1, - 1 - ) + mockFlow.value = WorkInfo( + uuid, + WorkInfo.State.SUCCEEDED, + emptySet(), + Data.EMPTY, + successProgressData, + 1, + 1 ) assertEquals(ShareExtensionProgressAction.ShowSuccessDialog(FileUploadType.USER), viewModel.events.value?.getContentIfNotHandled()) From 32e525abf1178eeb28017bd6c5b2e712af340183 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:33:37 +0200 Subject: [PATCH 47/94] Refactor BookmarksE2ETest to handle new routing logic (route to bokmark list instead of dashboard). (#3313) refs: MBL-19420 affects: Student release note: --- .../student/ui/e2e/compose/BookmarksE2ETest.kt | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt index cfdded6aaa..aee37835bc 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt @@ -25,7 +25,6 @@ import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.pressBackButton -import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.PagesApi @@ -91,21 +90,17 @@ class BookmarksE2ETest : StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert if the '$bookmarkName' bookmark is navigating to the Assignment Details page.") assignmentDetailsPage.assertAssignmentTitle(assignment.name) - Log.d(STEP_TAG, "Navigate back to the Dashboard page.") + Log.d(STEP_TAG, "Navigate back to the Bookmark List page.") Espresso.pressBack() - Log.d(STEP_TAG, "Click on the bookmark page's overflow menu.") - leftSideNavigationDrawerPage.clickBookmarksMenu() - - Log.d(ASSERTION_TAG, "Assert if the bookmark is displayed.") + Log.d(ASSERTION_TAG, "Assert if the '${bookmarkName}' bookmark is displayed.") bookmarkPage.assertBookmarkDisplayed(bookmarkName) val newName = "Assignment Details BM Modified" Log.d(STEP_TAG, "Change bookmark's name from '$bookmarkName' to '$newName'.") bookmarkPage.changeBookmarkName(bookmarkName, newName) - Log.d(ASSERTION_TAG, "Refresh bookmark page and assert if the bookmark's name has been changed.") - refresh() + Log.d(ASSERTION_TAG, "Assert if the '${bookmarkName}' bookmark's name has been changed to '$newName'.") bookmarkPage.assertBookmarkDisplayed(newName) Log.d(STEP_TAG, "Click on the previously renamed bookmark.") @@ -114,12 +109,9 @@ class BookmarksE2ETest : StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert if it's still navigating to the corresponding assignment's details page.") assignmentDetailsPage.assertAssignmentTitle(assignment.name) - Log.d(STEP_TAG, "Navigate back to the bookmark page.") + Log.d(STEP_TAG, "Navigate back to the Bookmark List page.") Espresso.pressBack() - Log.d(STEP_TAG, "Click on the bookmark page's overflow menu.") - leftSideNavigationDrawerPage.clickBookmarksMenu() - Log.d(STEP_TAG, "Delete bookmark: '$newName'.") bookmarkPage.deleteBookmark(newName) From 427dd4bc8942e2c6e5ef540c0ccb68923b68ea74 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:03:52 +0200 Subject: [PATCH 48/94] [MBL-19397][Parent] Observers unable to see Quiz submission feedback when "Assignment Enhancements" feature option is enable refs: MBL-19397 affects: Parent release note: Fixed parent observer quiz submission viewing for both classic and New Quizzes. --- .../details/ParentAssignmentDetailsRouter.kt | 5 +-- .../details/StudentAssignmentDetailsRouter.kt | 3 +- .../canvasapi2/models/Assignment.kt | 4 +++ .../canvasapi2/unit/AssignmentTest.kt | 34 +++++++++++++++++++ .../details/AssignmentDetailsFragment.kt | 3 +- .../details/AssignmentDetailsRouter.kt | 3 +- .../details/AssignmentDetailsViewData.kt | 3 +- .../details/AssignmentDetailsViewModel.kt | 6 ++-- .../details/AssignmentDetailsViewModelTest.kt | 6 ++-- 9 files changed, 57 insertions(+), 10 deletions(-) diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRouter.kt index 72389da24c..b93966da95 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRouter.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRouter.kt @@ -47,13 +47,14 @@ class ParentAssignmentDetailsRouter( assignmentUrl: String?, isAssignmentEnhancementEnabled: Boolean, isObserver: Boolean, - initialSelectedSubmissionAttempt: Long? + initialSelectedSubmissionAttempt: Long?, + isQuiz: Boolean ) { assignmentUrl ?: return val parentId = apiPrefs.user?.id ?: return val currentStudentId = parentPrefs.currentStudent?.id ?: return val cookies = mapOf("k5_observed_user_for_$parentId" to "$currentStudentId") - val url = if (isAssignmentEnhancementEnabled) { + val url = if (isAssignmentEnhancementEnabled && !isQuiz) { assignmentUrl } else { "$assignmentUrl/submissions/$currentStudentId" diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt index 085ee40b20..0f217c8b39 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt @@ -59,7 +59,8 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() { assignmentUrl: String?, isAssignmentEnhancementEnabled: Boolean, isObserver: Boolean, - initialSelectedSubmissionAttempt: Long? + initialSelectedSubmissionAttempt: Long?, + isQuiz: Boolean ) { RouteMatcher.route( activity, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt index 8e427574ee..468b004053 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt @@ -253,6 +253,10 @@ data class Assignment( } ?: LtiType.EXTERNAL_TOOL } + fun isQuiz(): Boolean { + return getSubmissionTypes().contains(SubmissionType.ONLINE_QUIZ) || ltiToolType() == LtiType.NEW_QUIZZES_LTI + } + companion object { const val PASS_FAIL_TYPE = "pass_fail" diff --git a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/AssignmentTest.kt b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/AssignmentTest.kt index 81185b1e4c..5ba712a56b 100644 --- a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/AssignmentTest.kt +++ b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/AssignmentTest.kt @@ -290,4 +290,38 @@ class AssignmentTest { } //endregion + //region isQuiz + @Test + fun isQuiz_TestOnlineQuiz() { + val assignment = Assignment(submissionTypesRaw = listOf("online_quiz")) + assertEquals(true, assignment.isQuiz()) + } + + @Test + fun isQuiz_TestQuizLtiAssignment() { + val externalToolAttributes = ExternalToolAttributes(url = "https://example.com/quiz-lti/launch") + val assignment = Assignment( + submissionTypesRaw = listOf("external_tool"), + externalToolAttributes = externalToolAttributes + ) + assertEquals(true, assignment.isQuiz()) + } + + @Test + fun isQuiz_TestRegularExternalTool() { + val externalToolAttributes = ExternalToolAttributes(url = "https://example.com/tool/launch") + val assignment = Assignment( + submissionTypesRaw = listOf("external_tool"), + externalToolAttributes = externalToolAttributes + ) + assertEquals(false, assignment.isQuiz()) + } + + @Test + fun isQuiz_TestNonQuizSubmissionType() { + val assignment = Assignment(submissionTypesRaw = listOf("online_upload")) + assertEquals(false, assignment.isQuiz()) + } + //endregion + } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt index 84ae5f008d..e9d46eb329 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt @@ -222,7 +222,8 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo action.assignmentUrl, action.isAssignmentEnhancementEnabled, action.isObserver, - action.selectedSubmissionAttempt + action.selectedSubmissionAttempt, + action.isQuiz ) } is AssignmentDetailAction.NavigateToQuizScreen -> { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsRouter.kt index 4f76e64761..2d33d43ae4 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsRouter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsRouter.kt @@ -40,7 +40,8 @@ open class AssignmentDetailsRouter { assignmentUrl: String?, isAssignmentEnhancementEnabled: Boolean, isObserver: Boolean = false, - initialSelectedSubmissionAttempt: Long? = null + initialSelectedSubmissionAttempt: Long? = null, + isQuiz: Boolean = false ) = Unit open fun navigateToQuizScreen(activity: FragmentActivity, canvasContext: CanvasContext, quiz: Quiz, url: String) = Unit diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt index a91907067a..1a990e04bc 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt @@ -69,7 +69,8 @@ sealed class AssignmentDetailAction { val isObserver: Boolean, val selectedSubmissionAttempt: Long?, val assignmentUrl: String?, - val isAssignmentEnhancementEnabled: Boolean + val isAssignmentEnhancementEnabled: Boolean, + val isQuiz: Boolean ) : AssignmentDetailAction() data class NavigateToQuizScreen(val quiz: Quiz) : AssignmentDetailAction() data class NavigateToDiscussionScreen(val discussionTopicHeaderId: Long, val course: Course) : AssignmentDetailAction() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt index fcbe41d10b..96ad2c8596 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt @@ -300,7 +300,8 @@ class AssignmentDetailsViewModel @Inject constructor( isObserver, submission.attempt, assignmentResult.htmlUrl, - isAssignmentEnhancementEnabled + isAssignmentEnhancementEnabled, + assignmentResult.isQuiz() ) ) } @@ -600,7 +601,8 @@ class AssignmentDetailsViewModel @Inject constructor( isObserver, selectedSubmission?.attempt, assignment?.htmlUrl, - isAssignmentEnhancementEnabled + isAssignmentEnhancementEnabled, + assignment?.isQuiz() == true ) ) } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt index 0943a58f14..aef2299e79 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModelTest.kt @@ -499,7 +499,8 @@ class AssignmentDetailsViewModelTest { isObserver = false, selectedSubmissionAttempt = null, assignmentUrl = "https://assignment.url", - isAssignmentEnhancementEnabled = true + isAssignmentEnhancementEnabled = true, + isQuiz = false ) assertEquals(expected, viewModel.events.value?.peekContent()) } @@ -1164,7 +1165,8 @@ class AssignmentDetailsViewModelTest { isObserver = false, selectedSubmissionAttempt = 2L, assignmentUrl = "https://assignment.url", - isAssignmentEnhancementEnabled = true + isAssignmentEnhancementEnabled = true, + isQuiz = false ) assertEquals(expected, viewModel.events.value?.peekContent()) } From 55b5386d98bb9c0b3d2be58e5d7489b1d414cd83 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:14:05 +0200 Subject: [PATCH 49/94] [MBL-19423] Fix random numbers being hyperlinked in Inbox messages (#3314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes an issue where random numbers (3+ digits) in Inbox messages were being incorrectly hyperlinked on Android apps, causing "Failed to Open URL" errors when clicked. ## Changes Updated `StringExtensions.kt` to use Android's built-in phone number filters (`Linkify.sPhoneNumberMatchFilter` and `Linkify.sPhoneNumberTransformFilter`) to distinguish actual phone numbers from random digit sequences. **Before:** Any sequence of 3+ digits (e.g., "123", "2024", "12345") was hyperlinked **After:** Only properly formatted phone numbers (e.g., "555-123-4567", "(555) 123-4567", "+1-555-123-4567") are hyperlinked ## Test plan 1. Send an Inbox message containing random numbers like "123", "2024", "12345" - ✅ Verify these are NOT hyperlinked 2. Send an Inbox message with formatted phone numbers like "555-123-4567", "(555) 123-4567", "+1-555-123-4567" - ✅ Verify these ARE hyperlinked and open properly 3. Test in Student, Teacher, and Parent apps 4. Verify web URLs and email addresses still work correctly refs: MBL-19423 affects: Student, Teacher, Parent release note: Fixed random numbers being incorrectly hyperlinked in Inbox messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- .../com/instructure/pandautils/utils/StringExtensions.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/StringExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/StringExtensions.kt index 222022c256..7e7666520e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/StringExtensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/StringExtensions.kt @@ -47,7 +47,13 @@ fun String.linkify( val spannable = SpannableString(this@linkify) Linkify.addLinks(spannable, Patterns.WEB_URL, null) Linkify.addLinks(spannable, Patterns.EMAIL_ADDRESS, null) - Linkify.addLinks(spannable, Patterns.PHONE, null) + Linkify.addLinks( + spannable, + Patterns.PHONE, + "tel:", + Linkify.sPhoneNumberMatchFilter, + Linkify.sPhoneNumberTransformFilter + ) val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java) for (span in spans) { From d4c1e6dbdb8d99675f49751fbfcf1aae92cec1a5 Mon Sep 17 00:00:00 2001 From: domonkosadam <53480952+domonkosadam@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:42:02 +0200 Subject: [PATCH 50/94] Fix glide compiler (#3318) --- libs/pandautils/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/pandautils/build.gradle b/libs/pandautils/build.gradle index 345053752e..850901e036 100644 --- a/libs/pandautils/build.gradle +++ b/libs/pandautils/build.gradle @@ -166,7 +166,7 @@ dependencies { api (Libs.GLIDE_OKHTTP) { exclude group: "com.android.support" } - ksp Libs.GLIDE_COMPILER + kapt Libs.GLIDE_COMPILER api Libs.ANDROID_SVG From 9abd2b510420a2819dd76b387fdc5583bc9ab8f3 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:16:08 +0200 Subject: [PATCH 51/94] [MBL-19351][Student] Display discussion checkpoint dates in module list (#3315) refs: MBL-19351 affects: Student release note: Discussion checkpoint dates now display in the module list * Changed item design * Added graphQL request and logic. * Bind correct data to the views. * Offline behavior * Unit and dao tests. * Interaction test * Fixed test. --- .../ui/interaction/ModuleInteractionTest.kt | 107 +- .../student/ui/pages/classic/ModulesPage.kt | 28 +- .../student/di/feature/ModuleListModule.kt | 20 +- .../modules/list/ModuleListRepository.kt | 15 + .../list/adapter/ModuleListRecyclerAdapter.kt | 57 +- .../modules/list/adapter/ModuleViewHolder.kt | 69 +- .../list/datasource/ModuleListDataSource.kt | 3 + .../datasource/ModuleListLocalDataSource.kt | 30 +- .../datasource/ModuleListNetworkDataSource.kt | 9 +- .../src/main/res/layout/viewholder_module.xml | 31 +- .../modules/list/ModuleListRepositoryTest.kt | 64 + .../ModuleListLocalDataSourceTest.kt | 104 +- .../ModuleListNetworkDataSourceTest.kt | 35 +- .../mockcanvas/fakes/FakeModuleManager.kt | 38 + .../ModuleItemCheckpointsQuery.graphql | 29 + .../com/instructure/canvasapi2/schema.json | 20 + .../di/graphql/ModuleManagerModule.kt | 36 + .../managers/graphql/ModuleManager.kt | 35 + .../managers/graphql/ModuleManagerImpl.kt | 66 + .../8.json | 6025 +++++++++++++++++ .../room/offline/daos/CheckpointDaoTest.kt | 222 + .../pandautils/di/OfflineSyncModule.kt | 10 +- .../features/offline/sync/CourseSync.kt | 35 +- .../room/offline/OfflineDatabase.kt | 2 +- .../room/offline/OfflineDatabaseMigrations.kt | 24 + .../room/offline/daos/CheckpointDao.kt | 3 + .../room/offline/entities/CheckpointEntity.kt | 24 +- 27 files changed, 7076 insertions(+), 65 deletions(-) create mode 100644 automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeModuleManager.kt create mode 100644 libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/ModuleItemCheckpointsQuery.graphql create mode 100644 libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/ModuleManagerModule.kt create mode 100644 libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManager.kt create mode 100644 libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManagerImpl.kt create mode 100644 libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/8.json diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt index 00b7bece2d..ce42ba33cf 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.platform.ComposeView import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.webdriver.Locator +import androidx.test.platform.app.InstrumentationRegistry 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.FeatureCategory @@ -38,9 +39,13 @@ import com.instructure.canvas.espresso.mockcanvas.addPageToCourse import com.instructure.canvas.espresso.mockcanvas.addQuestionToQuiz import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeModuleManager import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule +import com.instructure.canvasapi2.di.graphql.ModuleManagerModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint +import com.instructure.canvasapi2.managers.graphql.ModuleManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.DiscussionTopicHeader @@ -52,6 +57,7 @@ import com.instructure.canvasapi2.models.Page import com.instructure.canvasapi2.models.Quiz import com.instructure.canvasapi2.models.QuizAnswer import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.DateHelper import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 @@ -65,15 +71,20 @@ import dagger.hilt.android.testing.UninstallModules import org.hamcrest.Matchers import org.junit.Test import java.net.URLEncoder +import java.util.Date @HiltAndroidTest -@UninstallModules(CustomGradeStatusModule::class) +@UninstallModules(CustomGradeStatusModule::class, ModuleManagerModule::class) class ModuleInteractionTest : StudentComposeTest() { @BindValue @JvmField val customGradeStatusesManager: CustomGradeStatusesManager = FakeCustomGradeStatusesManager() + @BindValue + @JvmField + val moduleManager: ModuleManager = FakeModuleManager() + override fun displaysPageObjects() = Unit // Not used for interaction tests // A collection of things that we create during initialization and remember for use during @@ -625,6 +636,100 @@ class ModuleInteractionTest : StudentComposeTest() { modulesPage.assertPossiblePointsNotDisplayed(assignment.name.orEmpty()) } + // Discussion checkpoint dates should be displayed in module list + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION, SecondaryFeatureCategory.MODULES_DISCUSSIONS) + fun testModules_discussionCheckpointDatesDisplayed() { + // Set up all mock data BEFORE navigating to the page + val data = MockCanvas.init( + studentCount = 1, + courseCount = 1, + favoriteCourseCount = 1 + ) + + val course = data.courses.values.first() + val user = data.users.values.first() + + // Add a course tab + val modulesTab = Tab(position = 2, label = "Modules", visibility = "public", tabId = Tab.MODULES_ID) + data.courseTabs[course.id]!! += modulesTab + + // Create a module + data.addModuleToCourse( + course = course, + moduleName = "Big Module" + ) + + val module = data.courseModules[course.id]!!.first() + + // Create a discussion and add it as a module item + val discussionTitle = "Discussion with Checkpoints" + topicHeader = data.addDiscussionTopicToCourse( + course = course, + user = user, + topicTitle = discussionTitle, + topicDescription = "A discussion with checkpoints" + ) + data.addItemToModule( + course = course, + moduleId = module.id, + item = topicHeader!!, + moduleContentDetails = ModuleContentDetails() + ) + + // Get the module item ID from the module items + val updatedModule = data.courseModules[course.id]!!.first() + val discussionModuleItem = updatedModule.items.find { it.title == discussionTitle }!! + + // Set up fake checkpoint data BEFORE navigating to the page + val fakeModuleManager = moduleManager as FakeModuleManager + + val checkpointDate1 = Date(1750089600000L) // Jun 16, 2025 6:00 PM + val checkpointDate2 = Date(1750608000000L) // Jun 22, 2025 6:00 PM + + fakeModuleManager.setCheckpoints( + courseId = course.id.toString(), + moduleItemId = discussionModuleItem.id.toString(), + checkpoints = listOf( + ModuleItemCheckpoint( + dueAt = checkpointDate1, + tag = "reply_to_topic", + pointsPossible = 5.0 + ), + ModuleItemCheckpoint( + dueAt = checkpointDate2, + tag = "reply_to_entry", + pointsPossible = 5.0 + ) + ) + ) + + // NOW navigate to the modules page with the checkpoint data already set up + val student = data.students[0] + val token = data.tokenFor(student)!! + tokenLogin(data.domain, token, student) + dashboardPage.waitForRender() + + // Navigate to the course + dashboardPage.selectCourse(course) + // Navigate to the modules page - this will trigger the initial load with checkpoints + courseBrowserPage.selectModules() + + val expectedDateText1 = DateHelper.createPrefixedDateTimeString( + InstrumentationRegistry.getInstrumentation().targetContext, + R.string.toDoDue, + checkpointDate1 + ) + val expectedDateText2 = DateHelper.createPrefixedDateTimeString( + InstrumentationRegistry.getInstrumentation().targetContext, + R.string.toDoDue, + checkpointDate2 + ) + + // Assert that checkpoint dates are displayed + modulesPage.assertCheckpointDatesDisplayed(discussionTitle, listOf(expectedDateText1!!, expectedDateText2!!)) + } + // Mock a specified number of students and courses, add some assorted assignments, discussions, etc... // in the form of module items, and navigate to the modules page of the course private fun getToCourseModules( diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt index 36d7a8fe5d..3a2f540655 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt @@ -111,12 +111,38 @@ class ModulesPage : BasePage(R.id.modulesPage) { } fun assertPossiblePointsNotDisplayed(name: String) { - val matcher = withParent(hasSibling(withChild(withId(R.id.title) + withText(name)))) + withId(R.id.points) + val matcher = allOf( + withId(R.id.points), + hasSibling(withChild(withId(R.id.title) + withText(name))) + ) scrollRecyclerView(R.id.listView, matcher) onView(matcher).assertNotDisplayed() } + fun assertCheckpointDatesDisplayed(discussionTitle: String, expectedDateTexts: List) { + // First scroll to the discussion item + scrollRecyclerView(R.id.listView, withText(discussionTitle)) + + // Find the checkpointDatesContainer within the same parent as the title + val checkpointContainerMatcher = allOf( + withId(R.id.checkpointDatesContainer), + hasSibling(withChild(withId(R.id.title) + withText(discussionTitle))) + ) + + // Assert that the checkpoint dates container is visible (not GONE) + onView(checkpointContainerMatcher).check(matches(isDisplayed())) + + // Assert each expected date text is displayed within the container + expectedDateTexts.forEach { dateText -> + val dateTextMatcher = allOf( + withText(dateText), + withParent(checkpointContainerMatcher) + ) + onView(dateTextMatcher).check(matches(isDisplayed())) + } + } + /** * It is occasionally the case that we need to click a few extra buttons to get "fully" into * the item. Thus the [extraClickIds] vararg param. diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/ModuleListModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/ModuleListModule.kt index 4420911574..755260c17f 100644 --- a/apps/student/src/main/java/com/instructure/student/di/feature/ModuleListModule.kt +++ b/apps/student/src/main/java/com/instructure/student/di/feature/ModuleListModule.kt @@ -19,6 +19,8 @@ package com.instructure.student.di.feature import com.instructure.canvasapi2.apis.CourseAPI import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.apis.TabAPI +import com.instructure.canvasapi2.managers.graphql.ModuleManager +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.CourseSettingsDao import com.instructure.pandautils.room.offline.daos.TabDao import com.instructure.pandautils.room.offline.facade.ModuleFacade @@ -37,13 +39,23 @@ import dagger.hilt.android.components.FragmentComponent class ModuleListModule { @Provides - fun provideModuleListLocalDataSource(tabDao: TabDao, moduleFacade: ModuleFacade, courseSettingsDao: CourseSettingsDao): ModuleListLocalDataSource { - return ModuleListLocalDataSource(tabDao, moduleFacade, courseSettingsDao) + fun provideModuleListLocalDataSource( + tabDao: TabDao, + moduleFacade: ModuleFacade, + courseSettingsDao: CourseSettingsDao, + checkpointDao: CheckpointDao + ): ModuleListLocalDataSource { + return ModuleListLocalDataSource(tabDao, moduleFacade, courseSettingsDao, checkpointDao) } @Provides - fun provideModuleListNetworkDataSource(moduleApi: ModuleAPI.ModuleInterface, tabApi: TabAPI.TabsInterface, courseApi: CourseAPI.CoursesInterface): ModuleListNetworkDataSource { - return ModuleListNetworkDataSource(moduleApi, tabApi, courseApi) + fun provideModuleListNetworkDataSource( + moduleApi: ModuleAPI.ModuleInterface, + tabApi: TabAPI.TabsInterface, + courseApi: CourseAPI.CoursesInterface, + moduleManager: ModuleManager + ): ModuleListNetworkDataSource { + return ModuleListNetworkDataSource(moduleApi, tabApi, courseApi, moduleManager) } @Provides diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListRepository.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListRepository.kt index 4dbfd3dc89..e6149d52e9 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListRepository.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListRepository.kt @@ -16,6 +16,7 @@ */ package com.instructure.student.features.modules.list +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.ModuleItem @@ -23,6 +24,8 @@ import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.DataResult import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.Const.REPLY_TO_ENTRY +import com.instructure.pandautils.utils.Const.REPLY_TO_TOPIC import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.student.features.modules.list.datasource.ModuleListDataSource @@ -74,4 +77,16 @@ class ModuleListRepository( suspend fun getNextPageModuleItems(nextUrl: String, forceNetwork: Boolean): DataResult> { return networkDataSource.getNextPageModuleItems(nextUrl, forceNetwork) } + + suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List { + return dataSource().getModuleItemCheckpoints(courseId, forceNetwork).map { + it.copy(checkpoints = it.checkpoints.sortedBy { checkpoint -> + when (checkpoint.tag) { + REPLY_TO_TOPIC -> 0 + REPLY_TO_ENTRY -> 1 + else -> 2 + } + }) + } + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleListRecyclerAdapter.kt index de002820d8..78de33a3db 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleListRecyclerAdapter.kt @@ -27,6 +27,8 @@ import android.view.View import android.view.WindowManager import android.widget.ProgressBar import androidx.recyclerview.widget.RecyclerView +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings @@ -63,16 +65,32 @@ open class ModuleListRecyclerAdapter( private val repository: ModuleListRepository, private val lifecycleScope: CoroutineScope, private val adapterToFragmentCallback: ModuleAdapterToFragmentCallback? -) : ExpandableRecyclerAdapter(context, ModuleObject::class.java, ModuleItem::class.java) { +) : ExpandableRecyclerAdapter( + context, + ModuleObject::class.java, + ModuleItem::class.java +) { private var initialDataJob: Job? = null private var moduleObjectJob: Job? = null private val moduleFromNetworkOrDb = HashMap() private var courseSettings: CourseSettings? = null + private var moduleItemCheckpointsMap: Map> = emptyMap() /* For testing purposes only */ - protected constructor(context: Context, repository: ModuleListRepository, lifecycleScope: CoroutineScope) : this(CanvasContext.defaultCanvasContext(), context, false, repository, lifecycleScope, null) // Callback not needed for testing, cast to null + protected constructor( + context: Context, + repository: ModuleListRepository, + lifecycleScope: CoroutineScope + ) : this( + CanvasContext.defaultCanvasContext(), + context, + false, + repository, + lifecycleScope, + null + ) // Callback not needed for testing, cast to null init { viewHolderHeaderClicked = object : ViewHolderHeaderClicked { @@ -113,9 +131,12 @@ open class ModuleListRecyclerAdapter( val courseColor = courseContext.color val groupItemCount = getGroupItemCount(moduleObject) val itemPosition = storedIndexOfItem(moduleObject, moduleItem) + val checkpoints = moduleItemCheckpointsMap[moduleItem.id.toString()] - (holder as ModuleViewHolder).bind(moduleObject, moduleItem, context, adapterToFragmentCallback, courseColor, - itemPosition == 0, itemPosition == groupItemCount - 1, courseSettings?.restrictQuantitativeData.orDefault()) + (holder as ModuleViewHolder).bind( + moduleObject, moduleItem, context, adapterToFragmentCallback, courseColor, + itemPosition == 0, itemPosition == groupItemCount - 1, courseSettings?.restrictQuantitativeData.orDefault(), checkpoints + ) } } @@ -151,6 +172,16 @@ open class ModuleListRecyclerAdapter( super.refresh() } + private suspend fun fetchModuleItemCheckpoints() { + try { + val checkpoints = repository.getModuleItemCheckpoints(courseContext.id.toString(), true) + moduleItemCheckpointsMap = checkpoints.associate { it.moduleItemId to it.checkpoints } + } catch (e: Exception) { + FirebaseCrashlytics.getInstance().recordException(e) + moduleItemCheckpointsMap = emptyMap() + } + } + // region Expandable Callbacks override fun createGroupCallback(): GroupSortedList.GroupComparatorCallback { return object : GroupSortedList.GroupComparatorCallback { @@ -198,7 +229,10 @@ open class ModuleListRecyclerAdapter( dialog.setContentView(R.layout.progress_dialog) val currentColor = courseContext.color - (dialog.findViewById(R.id.progressBar) as ProgressBar).indeterminateDrawable.setColorFilter(currentColor, PorterDuff.Mode.SRC_ATOP) + (dialog.findViewById(R.id.progressBar) as ProgressBar).indeterminateDrawable.setColorFilter( + currentColor, + PorterDuff.Mode.SRC_ATOP + ) return dialog } @@ -266,7 +300,8 @@ open class ModuleListRecyclerAdapter( if (failedResult.response != null && errorCode == 504 && APIHelper.isCachedResponse(failedResult.response!!) - && !Utils.isNetworkAvailable(context)) { + && !Utils.isNetworkAvailable(context) + ) { expandGroup(moduleObject, isNotifyGroupChange) } } @@ -324,7 +359,7 @@ open class ModuleListRecyclerAdapter( } } } - if(!shouldExhaustPagination || result.linkHeaders.nextUrl == null) { + if (!shouldExhaustPagination || result.linkHeaders.nextUrl == null) { // If we should exhaust pagination wait until we are done exhausting pagination adapterToFragmentCallback?.onRefreshFinished() } @@ -338,6 +373,7 @@ open class ModuleListRecyclerAdapter( initialDataJob = lifecycleScope.tryLaunch { val tabs = repository.getTabs(courseContext, isRefresh) courseSettings = repository.loadCourseSettings(courseContext.id, isRefresh) + fetchModuleItemCheckpoints() // We only want to show modules if its a course nav option OR set to as the homepage if (tabs.find { it.tabId == "modules" } != null || (courseContext as Course).homePage?.apiString == "modules") { @@ -376,9 +412,10 @@ open class ModuleListRecyclerAdapter( } if (moduleObject.state != null && - moduleObject.state == ModuleObject.State.Locked.apiString && - getGroupItemCount(moduleObject) > 0 && - getItem(moduleObject, 0)?.type == ModuleObject.State.UnlockRequirements.apiString) { + moduleObject.state == ModuleObject.State.Locked.apiString && + getGroupItemCount(moduleObject) > 0 && + getItem(moduleObject, 0)?.type == ModuleObject.State.UnlockRequirements.apiString + ) { val reqs = StringBuilder() val ids = moduleObject.prerequisiteIds diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt index 8282540c4b..0080b2ca27 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt @@ -5,6 +5,7 @@ import android.graphics.Typeface import android.view.View import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint import com.instructure.canvasapi2.models.ModuleItem import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.utils.DateHelper @@ -13,7 +14,6 @@ import com.instructure.canvasapi2.utils.isValid import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.DP import com.instructure.pandautils.utils.setGone -import com.instructure.pandautils.utils.setInvisible import com.instructure.pandautils.utils.setTextForVisibility import com.instructure.pandautils.utils.setVisible import com.instructure.student.R @@ -33,7 +33,8 @@ class ModuleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { courseColor: Int, isFirstItem: Boolean, isLastItem: Boolean, - restrictQuantitativeData: Boolean + restrictQuantitativeData: Boolean, + checkpoints: List? ) = with(ViewholderModuleBinding.bind(itemView)) { val isLocked = ModuleUtility.isGroupLocked(moduleObject) @@ -135,41 +136,67 @@ class ModuleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { // Details val details = moduleItem.moduleDetails if (details != null) { - val hasDate: Boolean - val hasPoints: Boolean - if (details.dueDate != null) { - date.text = DateHelper.createPrefixedDateTimeString( - context, - R.string.toDoDue, - details.dueDate - ) - hasDate = true + // Handle checkpoints or regular due date + if (!checkpoints.isNullOrEmpty()) { + // Hide the single date field + date.setGone() + + // Clear previous checkpoint views + checkpointDatesContainer.removeAllViews() + checkpointDatesContainer.setVisible() + + // Create a TextView for each checkpoint + checkpoints.forEach { checkpoint -> + val checkpointDateView = android.widget.TextView(context).apply { + layoutParams = android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT + ) + setTextAppearance(R.style.AdapterItemDescriptionText) + text = if (checkpoint.dueAt != null) { + DateHelper.createPrefixedDateTimeString( + context, + R.string.toDoDue, + checkpoint.dueAt + ) + } else { + context.getString(R.string.toDoNoDueDate) + } + } + checkpointDatesContainer.addView(checkpointDateView) + } } else { - date.text = "" - hasDate = false + // Show single date field for regular module items + checkpointDatesContainer.setGone() + if (details.dueDate != null) { + date.text = DateHelper.createPrefixedDateTimeString( + context, + R.string.toDoDue, + details.dueDate + ) + date.setVisible() + } else { + date.text = "" + date.setGone() + } } + val pointsPossible = details.pointsPossible if (pointsPossible.isValid() && !restrictQuantitativeData) { points.text = context.getString( R.string.totalPoints, NumberHelper.formatDecimal(pointsPossible.toDouble(), 2, true) ) - hasPoints = true + points.setVisible() } else { points.text = "" - hasPoints = false - } - if (!hasDate && !hasPoints) { - date.setGone() points.setGone() - } else { - if (hasDate) date.setVisible() else date.setInvisible() - if (hasPoints) points.setVisible() else points.setInvisible() } } else { points.text = "" date.text = "" date.setGone() + checkpointDatesContainer.setGone() points.setGone() } BinderUtils.updateShadows(isFirstItem, isLastItem, shadowTop, shadowBottom) diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListDataSource.kt index 1826dea7f7..c55d9debe5 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListDataSource.kt @@ -16,6 +16,7 @@ */ package com.instructure.student.features.modules.list.datasource +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.ModuleItem @@ -34,4 +35,6 @@ interface ModuleListDataSource { suspend fun getFirstPageModuleItems(canvasContext: CanvasContext, moduleId: Long, forceNetwork: Boolean): DataResult> suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? + + suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSource.kt index 1a06c1c859..84940a4ca0 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSource.kt @@ -16,6 +16,7 @@ */ package com.instructure.student.features.modules.list.datasource +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.ModuleItem @@ -23,11 +24,18 @@ import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.ApiType import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.CourseSettingsDao import com.instructure.pandautils.room.offline.daos.TabDao import com.instructure.pandautils.room.offline.facade.ModuleFacade +import com.instructure.pandautils.utils.orDefault -class ModuleListLocalDataSource(private val tabDao: TabDao, private val moduleFacade: ModuleFacade, private val courseSettingsDao: CourseSettingsDao) : ModuleListDataSource { +class ModuleListLocalDataSource( + private val tabDao: TabDao, + private val moduleFacade: ModuleFacade, + private val courseSettingsDao: CourseSettingsDao, + private val checkpointDao: CheckpointDao +) : ModuleListDataSource { override suspend fun getAllModuleObjects(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { val moduleObjects = moduleFacade.getModuleObjects(canvasContext.id) @@ -43,7 +51,11 @@ class ModuleListLocalDataSource(private val tabDao: TabDao, private val moduleFa return DataResult.Success(tabDao.findByCourseId(canvasContext.id).map { it.toApiModel() }, apiType = ApiType.DB) } - override suspend fun getFirstPageModuleItems(canvasContext: CanvasContext, moduleId: Long, forceNetwork: Boolean): DataResult> { + override suspend fun getFirstPageModuleItems( + canvasContext: CanvasContext, + moduleId: Long, + forceNetwork: Boolean + ): DataResult> { val moduleItems = moduleFacade.getModuleItems(moduleId) return DataResult.Success(moduleItems, apiType = ApiType.DB) } @@ -51,4 +63,18 @@ class ModuleListLocalDataSource(private val tabDao: TabDao, private val moduleFa override suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { return courseSettingsDao.findByCourseId(courseId)?.toApiModel() } + + override suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List { + val checkpointEntities = checkpointDao.findByCourseIdWithModuleItem(courseId.toLongOrNull().orDefault()) + + return checkpointEntities + .filter { it.moduleItemId != null } + .groupBy { it.moduleItemId } + .map { (moduleItemId, checkpoints) -> + ModuleItemWithCheckpoints( + moduleItemId = moduleItemId.toString(), + checkpoints = checkpoints.map { it.toModuleItemCheckpoint() } + ) + } + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSource.kt index 76e6b84a65..f95e49841a 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSource.kt @@ -20,6 +20,8 @@ import com.instructure.canvasapi2.apis.CourseAPI import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.apis.TabAPI import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints +import com.instructure.canvasapi2.managers.graphql.ModuleManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.ModuleItem @@ -31,7 +33,8 @@ import com.instructure.canvasapi2.utils.depaginate class ModuleListNetworkDataSource( private val moduleApi: ModuleAPI.ModuleInterface, private val tabApi: TabAPI.TabsInterface, - private val courseApi: CourseAPI.CoursesInterface) : ModuleListDataSource { + private val courseApi: CourseAPI.CoursesInterface, + private val moduleManager: ModuleManager) : ModuleListDataSource { override suspend fun getAllModuleObjects(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) @@ -69,4 +72,8 @@ class ModuleListNetworkDataSource( val restParams = RestParams(isForceReadFromNetwork = forceNetwork) return courseApi.getCourseSettings(courseId, restParams).dataOrNull } + + override suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List { + return moduleManager.getModuleItemCheckpoints(courseId, forceNetwork) + } } \ No newline at end of file diff --git a/apps/student/src/main/res/layout/viewholder_module.xml b/apps/student/src/main/res/layout/viewholder_module.xml index bb75dcb69c..6b9c81b20d 100644 --- a/apps/student/src/main/res/layout/viewholder_module.xml +++ b/apps/student/src/main/res/layout/viewholder_module.xml @@ -71,25 +71,26 @@ style="@style/AdapterItemDescriptionText" tools:text="An assignment description with some length so we can ensure it looks amazing on every device!" /> - - - + tools:text="Due Feb 12 at 11:59 PM"/> - + - + diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/list/ModuleListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/list/ModuleListRepositoryTest.kt index 8a71e2e507..4b1257f3b9 100644 --- a/apps/student/src/test/java/com/instructure/student/features/modules/list/ModuleListRepositoryTest.kt +++ b/apps/student/src/test/java/com/instructure/student/features/modules/list/ModuleListRepositoryTest.kt @@ -16,6 +16,8 @@ */ package com.instructure.student.features.modules.list +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.ModuleItem @@ -209,4 +211,66 @@ class ModuleListRepositoryTest { Assert.assertFalse(result!!.restrictQuantitativeData) } + + @Test + fun `Get module item checkpoints from network when device is online`() = runTest { + val networkCheckpoints = listOf( + ModuleItemWithCheckpoints( + "1", + listOf( + ModuleItemCheckpoint(null, "reply_to_topic", 5.0), + ModuleItemCheckpoint(null, "reply_to_entry", 5.0) + ) + ) + ) + coEvery { networkDataSource.getModuleItemCheckpoints(any(), any()) } returns networkCheckpoints + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.getModuleItemCheckpoints("1", true) + + Assert.assertEquals(1, result.size) + Assert.assertEquals("1", result[0].moduleItemId) + Assert.assertEquals(2, result[0].checkpoints.size) + } + + @Test + fun `Get module item checkpoints from local when device is offline`() = runTest { + val localCheckpoints = listOf( + ModuleItemWithCheckpoints( + "2", + listOf( + ModuleItemCheckpoint(null, "reply_to_topic", 10.0) + ) + ) + ) + coEvery { localDataSource.getModuleItemCheckpoints(any(), any()) } returns localCheckpoints + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.getModuleItemCheckpoints("2", true) + + Assert.assertEquals(1, result.size) + Assert.assertEquals("2", result[0].moduleItemId) + } + + @Test + fun `Checkpoints are sorted by tag with reply_to_topic first, reply_to_entry second, others last`() = runTest { + val unsortedCheckpoints = listOf( + ModuleItemWithCheckpoints( + "1", + listOf( + ModuleItemCheckpoint(null, "reply_to_entry", 5.0), + ModuleItemCheckpoint(null, "other", 5.0), + ModuleItemCheckpoint(null, "reply_to_topic", 5.0) + ) + ) + ) + coEvery { networkDataSource.getModuleItemCheckpoints(any(), any()) } returns unsortedCheckpoints + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.getModuleItemCheckpoints("1", true) + + Assert.assertEquals("reply_to_topic", result[0].checkpoints[0].tag) + Assert.assertEquals("reply_to_entry", result[0].checkpoints[1].tag) + Assert.assertEquals("other", result[0].checkpoints[2].tag) + } } \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSourceTest.kt index 480116a8a3..4b89d7382a 100644 --- a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSourceTest.kt +++ b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSourceTest.kt @@ -23,8 +23,10 @@ import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.ApiType import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.CourseSettingsDao import com.instructure.pandautils.room.offline.daos.TabDao +import com.instructure.pandautils.room.offline.entities.CheckpointEntity import com.instructure.pandautils.room.offline.entities.CourseSettingsEntity import com.instructure.pandautils.room.offline.entities.TabEntity import com.instructure.pandautils.room.offline.facade.ModuleFacade @@ -33,15 +35,17 @@ import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test class ModuleListLocalDataSourceTest { private val tabDao = mockk(relaxed = true) private val moduleFacade = mockk(relaxed = true) - private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true) + private val courseSettingsDao = mockk(relaxed = true) + private val checkpointDao = mockk(relaxed = true) - private val dataSource = ModuleListLocalDataSource(tabDao, moduleFacade, courseSettingsDao) + private val dataSource = ModuleListLocalDataSource(tabDao, moduleFacade, courseSettingsDao, checkpointDao) @Test fun `getAllModuleObjects returns all module objects with DB api type`() = runTest { @@ -99,4 +103,100 @@ class ModuleListLocalDataSourceTest { assertEquals(expected, result) } + + @Test + fun `Get module item checkpoints groups by module item id`() = runTest { + val checkpointEntities = listOf( + CheckpointEntity( + id = 1, + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ), + CheckpointEntity( + id = 2, + assignmentId = null, + name = null, + tag = "reply_to_entry", + pointsPossible = 5.0, + dueAt = "2025-10-20T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + ) + coEvery { checkpointDao.findByCourseIdWithModuleItem(any()) } returns checkpointEntities + + val result = dataSource.getModuleItemCheckpoints("1", false) + + assertEquals(1, result.size) + assertEquals("100", result[0].moduleItemId) + assertEquals(2, result[0].checkpoints.size) + } + + @Test + fun `Get module item checkpoints filters out null module item ids`() = runTest { + val checkpointEntities = listOf( + CheckpointEntity( + id = 1, + assignmentId = 1L, + name = null, + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = null, + courseId = 1L + ) + ) + coEvery { checkpointDao.findByCourseIdWithModuleItem(any()) } returns checkpointEntities + + val result = dataSource.getModuleItemCheckpoints("1", false) + + assertTrue(result.isEmpty()) + } + + @Test + fun `Get module item checkpoints returns empty list when no checkpoints found`() = runTest { + coEvery { checkpointDao.findByCourseIdWithModuleItem(any()) } returns emptyList() + + val result = dataSource.getModuleItemCheckpoints("1", false) + + assertTrue(result.isEmpty()) + } + + @Test + fun `Get module item checkpoints converts checkpoint entities to api models`() = runTest { + val checkpointEntity = CheckpointEntity( + id = 1, + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + coEvery { checkpointDao.findByCourseIdWithModuleItem(any()) } returns listOf(checkpointEntity) + + val result = dataSource.getModuleItemCheckpoints("1", false) + + assertEquals(1, result.size) + assertEquals("reply_to_topic", result[0].checkpoints[0].tag) + assertEquals(10.0, result[0].checkpoints[0].pointsPossible, 0.01) + } } \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt index e1466f82d8..2aa7d4eaaa 100644 --- a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt +++ b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt @@ -19,6 +19,9 @@ package com.instructure.student.features.modules.list.datasource import com.instructure.canvasapi2.apis.CourseAPI import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.apis.TabAPI +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints +import com.instructure.canvasapi2.managers.graphql.ModuleManager import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.ModuleItem @@ -37,8 +40,9 @@ class ModuleListNetworkDataSourceTest { private val moduleApi: ModuleAPI.ModuleInterface = mockk(relaxed = true) private val tabApi: TabAPI.TabsInterface = mockk(relaxed = true) private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val moduleManager: ModuleManager = mockk(relaxed = true) - private val dataSource = ModuleListNetworkDataSource(moduleApi, tabApi, courseApi) + private val dataSource = ModuleListNetworkDataSource(moduleApi, tabApi, courseApi, moduleManager) @Test fun `Return failed result when getAllModuleObjects fails`() = runTest { @@ -183,4 +187,33 @@ class ModuleListNetworkDataSourceTest { Assert.assertNull(result) } + + @Test + fun `Get module item checkpoints returns data from module manager`() = runTest { + val expectedCheckpoints = listOf( + ModuleItemWithCheckpoints( + "1", + listOf( + ModuleItemCheckpoint(null, "reply_to_topic", 5.0), + ModuleItemCheckpoint(null, "reply_to_entry", 5.0) + ) + ) + ) + coEvery { moduleManager.getModuleItemCheckpoints(any(), any()) } returns expectedCheckpoints + + val result = dataSource.getModuleItemCheckpoints("123", true) + + Assert.assertEquals(1, result.size) + Assert.assertEquals("1", result[0].moduleItemId) + Assert.assertEquals(2, result[0].checkpoints.size) + } + + @Test + fun `Get module item checkpoints returns empty list when no checkpoints available`() = runTest { + coEvery { moduleManager.getModuleItemCheckpoints(any(), any()) } returns emptyList() + + val result = dataSource.getModuleItemCheckpoints("123", true) + + Assert.assertTrue(result.isEmpty()) + } } \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeModuleManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeModuleManager.kt new file mode 100644 index 0000000000..e40370129c --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeModuleManager.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvas.espresso.mockcanvas.fakes + +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints +import com.instructure.canvasapi2.managers.graphql.ModuleManager + +class FakeModuleManager : ModuleManager { + + var checkpointsMap: Map> = emptyMap() + + override suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List { + return checkpointsMap[courseId] ?: emptyList() + } + + fun setCheckpoints(courseId: String, moduleItemId: String, checkpoints: List) { + val moduleItemWithCheckpoints = ModuleItemWithCheckpoints( + moduleItemId = moduleItemId, + checkpoints = checkpoints + ) + checkpointsMap = checkpointsMap + (courseId to listOf(moduleItemWithCheckpoints)) + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/ModuleItemCheckpointsQuery.graphql b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/ModuleItemCheckpointsQuery.graphql new file mode 100644 index 0000000000..c04675189c --- /dev/null +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/ModuleItemCheckpointsQuery.graphql @@ -0,0 +1,29 @@ +query ModuleItemCheckpointsQuery($courseId: ID!, $pageSize: Int!, $nextCursor: String) { + course(id: $courseId) { + modulesConnection(first: $pageSize, after: $nextCursor) { + pageInfo { + endCursor + startCursor + hasNextPage + hasPreviousPage + } + edges { + node { + moduleItems { + _id + content { + ... on Discussion { + id + checkpoints { + dueAt + tag + pointsPossible + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/schema.json b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/schema.json index 0d6b41ac99..9beabebbb7 100644 --- a/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/schema.json +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/schema.json @@ -15143,6 +15143,26 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "checkpoints", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Checkpoint", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "childTopics", "description": null, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/ModuleManagerModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/ModuleManagerModule.kt new file mode 100644 index 0000000000..cd3ed918bd --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/ModuleManagerModule.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 - 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.canvasapi2.di.graphql + +import com.apollographql.apollo.ApolloClient +import com.instructure.canvasapi2.di.DefaultApolloClient +import com.instructure.canvasapi2.managers.graphql.ModuleManager +import com.instructure.canvasapi2.managers.graphql.ModuleManagerImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class ModuleManagerModule { + + @Provides + fun provideModuleManager(@DefaultApolloClient apolloClient: ApolloClient): ModuleManager { + return ModuleManagerImpl(apolloClient) + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManager.kt new file mode 100644 index 0000000000..a540088c21 --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManager.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 - 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.canvasapi2.managers.graphql + +import java.util.Date + +data class ModuleItemCheckpoint( + val dueAt: Date?, + val tag: String, + val pointsPossible: Double +) + +data class ModuleItemWithCheckpoints( + val moduleItemId: String, + val checkpoints: List +) + +interface ModuleManager { + + suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManagerImpl.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManagerImpl.kt new file mode 100644 index 0000000000..9ec0c994c2 --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManagerImpl.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 - 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.canvasapi2.managers.graphql + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.Optional +import com.instructure.canvasapi2.ModuleItemCheckpointsQuery +import com.instructure.canvasapi2.QLClientConfig +import com.instructure.canvasapi2.enqueueQuery + +class ModuleManagerImpl(private val apolloClient: ApolloClient) : ModuleManager { + override suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List { + var hasNextPage = true + var nextCursor: String? = null + val moduleItemsWithCheckpoints = mutableListOf() + + while (hasNextPage) { + val nextCursorParam = if (nextCursor != null) Optional.present(nextCursor) else Optional.absent() + val query = ModuleItemCheckpointsQuery(courseId, QLClientConfig.GRAPHQL_PAGE_SIZE, nextCursorParam) + val data = apolloClient.enqueueQuery(query, forceNetwork).data + val modulesConnection = data?.course?.modulesConnection + + val newItems = modulesConnection?.edges + ?.flatMap { edge -> + edge?.node?.moduleItems?.mapNotNull { moduleItem -> + val discussion = moduleItem.content?.onDiscussion + if (discussion != null && !discussion.checkpoints.isNullOrEmpty()) { + val checkpoints = discussion.checkpoints.map { checkpoint -> + ModuleItemCheckpoint( + dueAt = checkpoint.dueAt, + tag = checkpoint.tag, + pointsPossible = checkpoint.pointsPossible + ) + } + ModuleItemWithCheckpoints( + moduleItemId = moduleItem._id, + checkpoints = checkpoints + ) + } else { + null + } + } ?: emptyList() + } ?: emptyList() + + moduleItemsWithCheckpoints.addAll(newItems) + hasNextPage = modulesConnection?.pageInfo?.hasNextPage ?: false + nextCursor = modulesConnection?.pageInfo?.endCursor + } + + return moduleItemsWithCheckpoints + } +} \ No newline at end of file diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/8.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/8.json new file mode 100644 index 0000000000..e3de8642eb --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/8.json @@ -0,0 +1,6025 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "371b12b510101fec6ff4b7155ac8b092", + "entities": [ + { + "tableName": "AssignmentDueDateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `assignmentOverrideId` INTEGER, `dueAt` TEXT, `title` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `isBase` INTEGER NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isBase", + "columnName": "isBase", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `submissionTypesRaw` TEXT NOT NULL, `dueAt` TEXT, `pointsPossible` REAL NOT NULL, `courseId` INTEGER NOT NULL, `isGradeGroupsIndividually` INTEGER NOT NULL, `gradingType` TEXT, `needsGradingCount` INTEGER NOT NULL, `htmlUrl` TEXT, `url` TEXT, `quizId` INTEGER NOT NULL, `isUseRubricForGrading` INTEGER NOT NULL, `rubricSettingsId` INTEGER, `allowedExtensions` TEXT NOT NULL, `submissionId` INTEGER, `assignmentGroupId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `isPeerReviews` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `lockExplanation` TEXT, `discussionTopicHeaderId` INTEGER, `freeFormCriterionComments` INTEGER NOT NULL, `published` INTEGER NOT NULL, `groupCategoryId` INTEGER NOT NULL, `userSubmitted` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `onlyVisibleToOverrides` INTEGER NOT NULL, `anonymousPeerReviews` INTEGER NOT NULL, `moderatedGrading` INTEGER NOT NULL, `anonymousGrading` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `plannerOverrideId` INTEGER, `isStudioEnabled` INTEGER NOT NULL, `inClosedGradingPeriod` INTEGER NOT NULL, `annotatableAttachmentId` INTEGER NOT NULL, `anonymousSubmissions` INTEGER NOT NULL, `omitFromFinalGrade` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentGroupId`) REFERENCES `AssignmentGroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionTypesRaw", + "columnName": "submissionTypesRaw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeGroupsIndividually", + "columnName": "isGradeGroupsIndividually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingType", + "columnName": "gradingType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUseRubricForGrading", + "columnName": "isUseRubricForGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricSettingsId", + "columnName": "rubricSettingsId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "allowedExtensions", + "columnName": "allowedExtensions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPeerReviews", + "columnName": "isPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSubmitted", + "columnName": "userSubmitted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousPeerReviews", + "columnName": "anonymousPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moderatedGrading", + "columnName": "moderatedGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousGrading", + "columnName": "anonymousGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isStudioEnabled", + "columnName": "isStudioEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inClosedGradingPeriod", + "columnName": "inClosedGradingPeriod", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "annotatableAttachmentId", + "columnName": "annotatableAttachmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousSubmissions", + "columnName": "anonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "omitFromFinalGrade", + "columnName": "omitFromFinalGrade", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentGroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentGroupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `position` INTEGER NOT NULL, `groupWeight` REAL NOT NULL, `rules` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupWeight", + "columnName": "groupWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `title` TEXT, `dueAt` INTEGER, `isAllDay` INTEGER NOT NULL, `allDayDate` TEXT, `unlockAt` INTEGER, `lockAt` INTEGER, `courseSectionId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayDate", + "columnName": "allDayDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentRubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `rubricId` TEXT NOT NULL, PRIMARY KEY(`assignmentId`, `rubricId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricId", + "columnName": "rubricId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId", + "rubricId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentScoreStatisticsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `mean` REAL NOT NULL, `min` REAL NOT NULL, `max` REAL NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mean", + "columnName": "mean", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "min", + "columnName": "min", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "max", + "columnName": "max", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentSetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `scoringRangeId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `position` INTEGER NOT NULL, `masteryPathId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`masteryPathId`) REFERENCES `MasteryPathEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringRangeId", + "columnName": "scoringRangeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "masteryPathId", + "columnName": "masteryPathId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "MasteryPathEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "masteryPathId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `originalName` TEXT, `courseCode` TEXT, `startAt` TEXT, `endAt` TEXT, `syllabusBody` TEXT, `hideFinalGrades` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `license` TEXT NOT NULL, `termId` INTEGER, `needsGradingCount` INTEGER NOT NULL, `isApplyAssignmentGroupWeights` INTEGER NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, `isFavorite` INTEGER NOT NULL, `accessRestrictedByDate` INTEGER NOT NULL, `imageUrl` TEXT, `bannerImageUrl` TEXT, `isWeightedGradingPeriods` INTEGER NOT NULL, `hasGradingPeriods` INTEGER NOT NULL, `homePage` TEXT, `restrictEnrollmentsToCourseDate` INTEGER NOT NULL, `workflowState` TEXT, `homeroomCourse` INTEGER NOT NULL, `courseColor` TEXT, `gradingScheme` TEXT, `pointsBasedGradingScheme` INTEGER NOT NULL, `scalingFactor` REAL NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`termId`) REFERENCES `TermEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syllabusBody", + "columnName": "syllabusBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideFinalGrades", + "columnName": "hideFinalGrades", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "termId", + "columnName": "termId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isApplyAssignmentGroupWeights", + "columnName": "isApplyAssignmentGroupWeights", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessRestrictedByDate", + "columnName": "accessRestrictedByDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bannerImageUrl", + "columnName": "bannerImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isWeightedGradingPeriods", + "columnName": "isWeightedGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasGradingPeriods", + "columnName": "hasGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homePage", + "columnName": "homePage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "restrictEnrollmentsToCourseDate", + "columnName": "restrictEnrollmentsToCourseDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeroomCourse", + "columnName": "homeroomCourse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseColor", + "columnName": "courseColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "gradingScheme", + "columnName": "gradingScheme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsBasedGradingScheme", + "columnName": "pointsBasedGradingScheme", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scalingFactor", + "columnName": "scalingFactor", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "TermEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "termId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFilesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`courseId`, `url`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "url" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "CourseGradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `gradingPeriodId` INTEGER NOT NULL, PRIMARY KEY(`courseId`, `gradingPeriodId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`gradingPeriodId`) REFERENCES `GradingPeriodEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "gradingPeriodId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "GradingPeriodEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "gradingPeriodId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseSummary` INTEGER, `restrictQuantitativeData` INTEGER NOT NULL, PRIMARY KEY(`courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseSummary", + "columnName": "courseSummary", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "restrictQuantitativeData", + "columnName": "restrictQuantitativeData", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `fullContentSync` INTEGER NOT NULL, `tabs` TEXT NOT NULL, `fullFileSync` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullContentSync", + "columnName": "fullContentSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullFileSync", + "columnName": "fullFileSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DashboardCardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isK5Subject` INTEGER NOT NULL, `shortName` TEXT, `originalName` TEXT, `courseCode` TEXT, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isK5Subject", + "columnName": "isK5Subject", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionEntryAttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionEntryId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionEntryId`, `remoteFileId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionEntryId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` TEXT, `createdAt` TEXT, `authorId` INTEGER, `description` TEXT, `userId` INTEGER NOT NULL, `parentId` INTEGER NOT NULL, `message` TEXT, `deleted` INTEGER NOT NULL, `totalChildren` INTEGER NOT NULL, `unreadChildren` INTEGER NOT NULL, `ratingCount` INTEGER NOT NULL, `ratingSum` INTEGER NOT NULL, `editorId` INTEGER NOT NULL, `_hasRated` INTEGER NOT NULL, `replyIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChildren", + "columnName": "totalChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadChildren", + "columnName": "unreadChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingCount", + "columnName": "ratingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingSum", + "columnName": "ratingSum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editorId", + "columnName": "editorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_hasRated", + "columnName": "_hasRated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyIds", + "columnName": "replyIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionParticipantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `pronouns` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionTopicHeaderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `discussionType` TEXT, `title` TEXT, `message` TEXT, `htmlUrl` TEXT, `postedDate` INTEGER, `delayedPostDate` INTEGER, `lastReplyDate` INTEGER, `requireInitialPost` INTEGER NOT NULL, `discussionSubentryCount` INTEGER NOT NULL, `readState` TEXT, `unreadCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `assignmentId` INTEGER, `locked` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `pinned` INTEGER NOT NULL, `authorId` INTEGER, `podcastUrl` TEXT, `groupCategoryId` TEXT, `announcement` INTEGER NOT NULL, `permissionId` INTEGER, `published` INTEGER NOT NULL, `allowRating` INTEGER NOT NULL, `onlyGradersCanRate` INTEGER NOT NULL, `sortByRating` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `lockAt` INTEGER, `userCanSeePosts` INTEGER NOT NULL, `specificSections` TEXT, `anonymousState` TEXT, `replyRequiredCount` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`authorId`) REFERENCES `DiscussionParticipantEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`permissionId`) REFERENCES `DiscussionTopicPermissionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionType", + "columnName": "discussionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedDate", + "columnName": "postedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "delayedPostDate", + "columnName": "delayedPostDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastReplyDate", + "columnName": "lastReplyDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "requireInitialPost", + "columnName": "requireInitialPost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionSubentryCount", + "columnName": "discussionSubentryCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readState", + "columnName": "readState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unreadCount", + "columnName": "unreadCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "podcastUrl", + "columnName": "podcastUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "announcement", + "columnName": "announcement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissionId", + "columnName": "permissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowRating", + "columnName": "allowRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyGradersCanRate", + "columnName": "onlyGradersCanRate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortByRating", + "columnName": "sortByRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userCanSeePosts", + "columnName": "userCanSeePosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "specificSections", + "columnName": "specificSections", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "anonymousState", + "columnName": "anonymousState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyRequiredCount", + "columnName": "replyRequiredCount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionParticipantEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "authorId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "DiscussionTopicPermissionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "permissionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `discussionTopicHeaderId` INTEGER NOT NULL, `attach` INTEGER NOT NULL, `update` INTEGER NOT NULL, `delete` INTEGER NOT NULL, `reply` INTEGER NOT NULL, FOREIGN KEY(`discussionTopicHeaderId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attach", + "columnName": "attach", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delete", + "columnName": "delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicHeaderId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicRemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionId`, `remoteFileId`), FOREIGN KEY(`discussionId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionId", + "columnName": "discussionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicSectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionTopicId` INTEGER NOT NULL, `sectionId` INTEGER NOT NULL, PRIMARY KEY(`discussionTopicId`, `sectionId`), FOREIGN KEY(`discussionTopicId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionTopicId", + "columnName": "discussionTopicId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionTopicId", + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `role` TEXT NOT NULL, `type` TEXT NOT NULL, `courseId` INTEGER, `courseSectionId` INTEGER, `enrollmentState` TEXT, `userId` INTEGER NOT NULL, `computedCurrentScore` REAL, `computedFinalScore` REAL, `computedCurrentGrade` TEXT, `computedFinalGrade` TEXT, `multipleGradingPeriodsEnabled` INTEGER NOT NULL, `totalsForAllGradingPeriodsOption` INTEGER NOT NULL, `currentPeriodComputedCurrentScore` REAL, `currentPeriodComputedFinalScore` REAL, `currentPeriodComputedCurrentGrade` TEXT, `currentPeriodComputedFinalGrade` TEXT, `currentGradingPeriodId` INTEGER NOT NULL, `currentGradingPeriodTitle` TEXT, `associatedUserId` INTEGER NOT NULL, `lastActivityAt` INTEGER, `limitPrivilegesToCourseSection` INTEGER NOT NULL, `observedUserId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`observedUserId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseSectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "computedCurrentScore", + "columnName": "computedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedFinalScore", + "columnName": "computedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedCurrentGrade", + "columnName": "computedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "computedFinalGrade", + "columnName": "computedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "multipleGradingPeriodsEnabled", + "columnName": "multipleGradingPeriodsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalsForAllGradingPeriodsOption", + "columnName": "totalsForAllGradingPeriodsOption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPeriodComputedCurrentScore", + "columnName": "currentPeriodComputedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalScore", + "columnName": "currentPeriodComputedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedCurrentGrade", + "columnName": "currentPeriodComputedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalGrade", + "columnName": "currentPeriodComputedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentGradingPeriodId", + "columnName": "currentGradingPeriodId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentGradingPeriodTitle", + "columnName": "currentGradingPeriodTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "associatedUserId", + "columnName": "associatedUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivityAt", + "columnName": "lastActivityAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "limitPrivilegesToCourseSection", + "columnName": "limitPrivilegesToCourseSection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "observedUserId", + "columnName": "observedUserId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "observedUserId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "courseSectionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileFolderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `createdDate` INTEGER, `updatedDate` INTEGER, `unlockDate` INTEGER, `lockDate` INTEGER, `isLocked` INTEGER NOT NULL, `isHidden` INTEGER NOT NULL, `isLockedForUser` INTEGER NOT NULL, `isHiddenForUser` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `size` INTEGER NOT NULL, `contentType` TEXT, `url` TEXT, `displayName` TEXT, `thumbnailUrl` TEXT, `parentFolderId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `filesCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `foldersCount` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `foldersUrl` TEXT, `filesUrl` TEXT, `fullName` TEXT, `forSubmissions` INTEGER NOT NULL, `canUpload` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedDate", + "columnName": "updatedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unlockDate", + "columnName": "unlockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockDate", + "columnName": "lockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLockedForUser", + "columnName": "isLockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHiddenForUser", + "columnName": "isHiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filesCount", + "columnName": "filesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "foldersCount", + "columnName": "foldersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "foldersUrl", + "columnName": "foldersUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesUrl", + "columnName": "filesUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "forSubmissions", + "columnName": "forSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canUpload", + "columnName": "canUpload", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EditDashboardItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `name` TEXT NOT NULL, `isFavorite` INTEGER NOT NULL, `enrollmentState` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ExternalToolAttributesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `url` TEXT, `newTab` INTEGER NOT NULL, `resourceLinkid` TEXT, `contentId` INTEGER, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "newTab", + "columnName": "newTab", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceLinkid", + "columnName": "resourceLinkid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`enrollmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, PRIMARY KEY(`enrollmentId`), FOREIGN KEY(`enrollmentId`) REFERENCES `EnrollmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "enrollmentId", + "columnName": "enrollmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "enrollmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "EnrollmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "enrollmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `startDate` TEXT, `endDate` TEXT, `weight` REAL NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `avatarUrl` TEXT, `isPublic` INTEGER NOT NULL, `membersCount` INTEGER NOT NULL, `joinLevel` TEXT, `courseId` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `role` TEXT, `groupCategoryId` INTEGER NOT NULL, `storageQuotaMb` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `concluded` INTEGER NOT NULL, `canAccess` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinLevel", + "columnName": "joinLevel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "storageQuotaMb", + "columnName": "storageQuotaMb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concluded", + "columnName": "concluded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canAccess", + "columnName": "canAccess", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupUserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LocalFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `createdDate` INTEGER NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MasteryPathAssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `overrideId` INTEGER NOT NULL, `assignmentSetId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentSetId`) REFERENCES `AssignmentSetEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "overrideId", + "columnName": "overrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentSetId", + "columnName": "assignmentSetId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentSetEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MasteryPathEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `selectedSetId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedSetId", + "columnName": "selectedSetId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleContentDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pointsPossible` TEXT, `dueAt` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `moduleId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `title` TEXT, `indent` INTEGER NOT NULL, `type` TEXT, `htmlUrl` TEXT, `url` TEXT, `published` INTEGER, `contentId` INTEGER NOT NULL, `externalUrl` TEXT, `pageUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`moduleId`) REFERENCES `ModuleObjectEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "indent", + "columnName": "indent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageUrl", + "columnName": "pageUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleObjectEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleObjectEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `name` TEXT, `unlockAt` TEXT, `sequentialProgress` INTEGER NOT NULL, `prerequisiteIds` TEXT, `state` TEXT, `completedAt` TEXT, `published` INTEGER, `itemCount` INTEGER NOT NULL, `itemsUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sequentialProgress", + "columnName": "sequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prerequisiteIds", + "columnName": "prerequisiteIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "completedAt", + "columnName": "completedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemCount", + "columnName": "itemCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemsUrl", + "columnName": "itemsUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "NeedsGradingCountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sectionId` INTEGER NOT NULL, `needsGradingCount` INTEGER NOT NULL, PRIMARY KEY(`sectionId`), FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `createdAt` INTEGER, `updatedAt` INTEGER, `hideFromStudents` INTEGER NOT NULL, `status` TEXT, `body` TEXT, `frontPage` INTEGER NOT NULL, `published` INTEGER NOT NULL, `editingRoles` TEXT, `htmlUrl` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hideFromStudents", + "columnName": "hideFromStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "frontPage", + "columnName": "frontPage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editingRoles", + "columnName": "editingRoles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER, `groupId` INTEGER, `userId` INTEGER, `contextType` TEXT, `contextName` TEXT, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `plannableTitle` TEXT, `plannableDetails` TEXT, `plannableTodoDate` TEXT, `plannableEndAt` INTEGER, `plannableAllDay` INTEGER, `plannableCourseId` INTEGER, `plannableGroupId` INTEGER, `plannableUserId` INTEGER, `plannableDate` INTEGER NOT NULL, `htmlUrl` TEXT, `submissionStateSubmitted` INTEGER, `submissionStateExcused` INTEGER, `submissionStateGraded` INTEGER, `newActivity` INTEGER, `plannerOverrideId` INTEGER, `plannerOverrideMarkedComplete` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextName", + "columnName": "contextName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableTitle", + "columnName": "plannableTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableDetails", + "columnName": "plannableDetails", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableTodoDate", + "columnName": "plannableTodoDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableEndAt", + "columnName": "plannableEndAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableAllDay", + "columnName": "plannableAllDay", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableCourseId", + "columnName": "plannableCourseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableGroupId", + "columnName": "plannableGroupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableUserId", + "columnName": "plannableUserId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableDate", + "columnName": "plannableDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionStateSubmitted", + "columnName": "submissionStateSubmitted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionStateExcused", + "columnName": "submissionStateExcused", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionStateGraded", + "columnName": "submissionStateGraded", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "newActivity", + "columnName": "newActivity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannerOverrideMarkedComplete", + "columnName": "plannerOverrideMarkedComplete", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, `markedComplete` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "markedComplete", + "columnName": "markedComplete", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `displayName` TEXT, `fileName` TEXT, `contentType` TEXT, `url` TEXT, `size` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `unlockAt` TEXT, `locked` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `lockAt` TEXT, `hiddenForUser` INTEGER NOT NULL, `thumbnailUrl` TEXT, `modifiedAt` TEXT, `lockedForUser` INTEGER NOT NULL, `previewUrl` TEXT, `lockExplanation` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hiddenForUser", + "columnName": "hiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "modifiedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RubricCriterionAssessmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `assignmentId` INTEGER NOT NULL, `ratingId` TEXT, `points` REAL, `comments` TEXT, PRIMARY KEY(`id`, `assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingId", + "columnName": "ratingId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "comments", + "columnName": "comments", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `criterionUseRange` INTEGER NOT NULL, `ignoreForScoring` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "criterionUseRange", + "columnName": "criterionUseRange", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoreForScoring", + "columnName": "ignoreForScoring", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionRatingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `rubricCriterionId` TEXT NOT NULL, PRIMARY KEY(`id`, `rubricCriterionId`), FOREIGN KEY(`rubricCriterionId`) REFERENCES `RubricCriterionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rubricCriterionId", + "columnName": "rubricCriterionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "rubricCriterionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "RubricCriterionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "rubricCriterionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `pointsPossible` REAL NOT NULL, `title` TEXT NOT NULL, `isReusable` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `isReadOnly` INTEGER NOT NULL, `freeFormCriterionComments` INTEGER NOT NULL, `hideScoreTotal` INTEGER NOT NULL, `hidePoints` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isReusable", + "columnName": "isReusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isReadOnly", + "columnName": "isReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideScoreTotal", + "columnName": "hideScoreTotal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidePoints", + "columnName": "hidePoints", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemAssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentOverrideId` INTEGER NOT NULL, `scheduleItemId` TEXT NOT NULL, PRIMARY KEY(`assignmentOverrideId`, `scheduleItemId`), FOREIGN KEY(`assignmentOverrideId`) REFERENCES `AssignmentOverrideEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`scheduleItemId`) REFERENCES `ScheduleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduleItemId", + "columnName": "scheduleItemId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentOverrideId", + "scheduleItemId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentOverrideEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentOverrideId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ScheduleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "scheduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `description` TEXT, `startAt` TEXT, `endAt` TEXT, `isAllDay` INTEGER NOT NULL, `allDayAt` TEXT, `locationAddress` TEXT, `locationName` TEXT, `htmlUrl` TEXT, `contextCode` TEXT, `effectiveContextCode` TEXT, `isHidden` INTEGER NOT NULL, `importantDates` INTEGER NOT NULL, `assignmentId` INTEGER, `type` TEXT NOT NULL, `itemType` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayAt", + "columnName": "allDayAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationAddress", + "columnName": "locationAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationName", + "columnName": "locationName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextCode", + "columnName": "contextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effectiveContextCode", + "columnName": "effectiveContextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "importantDates", + "columnName": "importantDates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemType", + "columnName": "itemType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER, `startAt` TEXT, `endAt` TEXT, `totalStudents` INTEGER NOT NULL, `restrictEnrollmentsToSectionDates` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "totalStudents", + "columnName": "totalStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "restrictEnrollmentsToSectionDates", + "columnName": "restrictEnrollmentsToSectionDates", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionDiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`submissionId` INTEGER NOT NULL, `discussionEntryId` INTEGER NOT NULL, PRIMARY KEY(`submissionId`, `discussionEntryId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "submissionId", + "discussionEntryId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `attempt` INTEGER NOT NULL, `submittedAt` INTEGER, `commentCreated` INTEGER, `mediaContentType` TEXT, `mediaCommentUrl` TEXT, `mediaCommentDisplay` TEXT, `body` TEXT, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, `workflowState` TEXT, `submissionType` TEXT, `previewUrl` TEXT, `url` TEXT, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `mediaCommentId` TEXT, `assignmentId` INTEGER NOT NULL, `userId` INTEGER, `graderId` INTEGER, `groupId` INTEGER, `pointsDeducted` REAL, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `postedAt` INTEGER, `gradingPeriodId` INTEGER, `customGradeStatusId` INTEGER, `hasSubAssignmentSubmissions` INTEGER NOT NULL, PRIMARY KEY(`id`, `attempt`), FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submittedAt", + "columnName": "submittedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "commentCreated", + "columnName": "commentCreated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaContentType", + "columnName": "mediaContentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentUrl", + "columnName": "mediaCommentUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentDisplay", + "columnName": "mediaCommentDisplay", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionType", + "columnName": "submissionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "graderId", + "columnName": "graderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pointsDeducted", + "columnName": "pointsDeducted", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasSubAssignmentSubmissions", + "columnName": "hasSubAssignmentSubmissions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "attempt" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `autoSyncEnabled` INTEGER NOT NULL, `syncFrequency` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoSyncEnabled", + "columnName": "autoSyncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncFrequency", + "columnName": "syncFrequency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifiOnly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TabEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `label` TEXT, `type` TEXT NOT NULL, `htmlUrl` TEXT, `externalUrl` TEXT, `visibility` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL, `ltiUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ltiUrl", + "columnName": "ltiUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "TermEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `startAt` TEXT, `endAt` TEXT, `isGroupTerm` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGroupTerm", + "columnName": "isGroupTerm", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserCalendarEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ics` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ics", + "columnName": "ics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `shortName` TEXT, `loginId` TEXT, `avatarUrl` TEXT, `primaryEmail` TEXT, `email` TEXT, `sortableName` TEXT, `bio` TEXT, `enrollmentIndex` INTEGER NOT NULL, `lastLogin` TEXT, `locale` TEXT, `effective_locale` TEXT, `pronouns` TEXT, `k5User` INTEGER NOT NULL, `rootAccount` TEXT, `isFakeStudent` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginId", + "columnName": "loginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "primaryEmail", + "columnName": "primaryEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortableName", + "columnName": "sortableName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bio", + "columnName": "bio", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enrollmentIndex", + "columnName": "enrollmentIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastLogin", + "columnName": "lastLogin", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effective_locale", + "columnName": "effective_locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "k5User", + "columnName": "k5User", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rootAccount", + "columnName": "rootAccount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFakeStudent", + "columnName": "isFakeStudent", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "QuizEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `mobileUrl` TEXT, `htmlUrl` TEXT, `description` TEXT, `quizType` TEXT, `assignmentGroupId` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `questionCount` INTEGER NOT NULL, `pointsPossible` TEXT, `isLockQuestionsAfterAnswering` INTEGER NOT NULL, `dueAt` TEXT, `timeLimit` INTEGER NOT NULL, `shuffleAnswers` INTEGER NOT NULL, `showCorrectAnswers` INTEGER NOT NULL, `scoringPolicy` TEXT, `accessCode` TEXT, `ipFilter` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `hideResults` TEXT, `showCorrectAnswersAt` TEXT, `hideCorrectAnswersAt` TEXT, `unlockAt` TEXT, `oneTimeResults` INTEGER NOT NULL, `lockAt` TEXT, `questionTypes` TEXT NOT NULL, `hasAccessCode` INTEGER NOT NULL, `oneQuestionAtATime` INTEGER NOT NULL, `requireLockdownBrowser` INTEGER NOT NULL, `requireLockdownBrowserForResults` INTEGER NOT NULL, `allowAnonymousSubmissions` INTEGER NOT NULL, `published` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `isOnlyVisibleToOverrides` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mobileUrl", + "columnName": "mobileUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizType", + "columnName": "quizType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionCount", + "columnName": "questionCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLockQuestionsAfterAnswering", + "columnName": "isLockQuestionsAfterAnswering", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeLimit", + "columnName": "timeLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shuffleAnswers", + "columnName": "shuffleAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showCorrectAnswers", + "columnName": "showCorrectAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringPolicy", + "columnName": "scoringPolicy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accessCode", + "columnName": "accessCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ipFilter", + "columnName": "ipFilter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideResults", + "columnName": "hideResults", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showCorrectAnswersAt", + "columnName": "showCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideCorrectAnswersAt", + "columnName": "hideCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oneTimeResults", + "columnName": "oneTimeResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "questionTypes", + "columnName": "questionTypes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasAccessCode", + "columnName": "hasAccessCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneQuestionAtATime", + "columnName": "oneQuestionAtATime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowser", + "columnName": "requireLockdownBrowser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowserForResults", + "columnName": "requireLockdownBrowserForResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowAnonymousSubmissions", + "columnName": "allowAnonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnlyVisibleToOverrides", + "columnName": "isOnlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockInfoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `modulePrerequisiteNames` TEXT, `unlockAt` TEXT, `lockedModuleId` INTEGER, `assignmentId` INTEGER, `moduleId` INTEGER, `pageId` INTEGER, FOREIGN KEY(`moduleId`) REFERENCES `ModuleContentDetailsEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`pageId`) REFERENCES `PageEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modulePrerequisiteNames", + "columnName": "modulePrerequisiteNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleContentDetailsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "PageEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockedModuleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `unlockAt` TEXT, `isRequireSequentialProgress` INTEGER NOT NULL, `lockInfoId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`lockInfoId`) REFERENCES `LockInfoEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isRequireSequentialProgress", + "columnName": "isRequireSequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockInfoId", + "columnName": "lockInfoId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockInfoEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockInfoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleNameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `lockedModuleId` INTEGER NOT NULL, FOREIGN KEY(`lockedModuleId`) REFERENCES `LockedModuleEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockedModuleEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockedModuleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleCompletionRequirementEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT, `minScore` REAL NOT NULL, `maxScore` REAL NOT NULL, `completed` INTEGER, `moduleId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "minScore", + "columnName": "minScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxScore", + "columnName": "maxScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `fileName` TEXT, `courseId` INTEGER NOT NULL, `url` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "ConferenceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `conferenceKey` TEXT, `conferenceType` TEXT, `description` TEXT, `duration` INTEGER NOT NULL, `endedAt` INTEGER, `hasAdvancedSettings` INTEGER NOT NULL, `joinUrl` TEXT, `longRunning` INTEGER NOT NULL, `startedAt` INTEGER, `title` TEXT, `url` TEXT, `contextType` TEXT NOT NULL, `contextId` INTEGER NOT NULL, `record` INTEGER, `users` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conferenceKey", + "columnName": "conferenceKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conferenceType", + "columnName": "conferenceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAdvancedSettings", + "columnName": "hasAdvancedSettings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinUrl", + "columnName": "joinUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longRunning", + "columnName": "longRunning", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startedAt", + "columnName": "startedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "record", + "columnName": "record", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "users", + "columnName": "users", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ConferenceRecordingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recordingId` TEXT NOT NULL, `conferenceId` INTEGER NOT NULL, `createdAtMillis` INTEGER NOT NULL, `durationMinutes` INTEGER NOT NULL, `playbackUrl` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`recordingId`), FOREIGN KEY(`conferenceId`) REFERENCES `ConferenceEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "recordingId", + "columnName": "recordingId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conferenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtMillis", + "columnName": "createdAtMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durationMinutes", + "columnName": "durationMinutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playbackUrl", + "columnName": "playbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recordingId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ConferenceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "conferenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFeaturesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `features` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionCommentId`) REFERENCES `SubmissionCommentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionCommentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionCommentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `submissionId` INTEGER NOT NULL, `attemptId` INTEGER NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "DiscussionTopicEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unreadEntries` TEXT NOT NULL, `participantIds` TEXT NOT NULL, `viewIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadEntries", + "columnName": "unreadEntries", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantIds", + "columnName": "participantIds", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewIds", + "columnName": "viewIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CourseSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `tabs` TEXT NOT NULL, `additionalFilesStarted` INTEGER NOT NULL, `progressState` TEXT NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalFilesStarted", + "columnName": "additionalFilesStarted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalFile", + "columnName": "additionalFile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncProgressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "StudioMediaProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ltiLaunchId` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "ltiLaunchId", + "columnName": "ltiLaunchId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomGradeStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CheckpointEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `assignmentId` INTEGER, `name` TEXT, `tag` TEXT, `pointsPossible` REAL, `dueAt` TEXT, `onlyVisibleToOverrides` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `moduleItemId` INTEGER, `courseId` INTEGER, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`moduleItemId`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "moduleItemId", + "columnName": "moduleItemId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubAssignmentSubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `submissionId` INTEGER NOT NULL, `submissionAttempt` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `latePolicyStatus` TEXT, `customGradeStatusId` INTEGER, `subAssignmentTag` TEXT, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `userId` INTEGER NOT NULL, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, FOREIGN KEY(`submissionId`, `submissionAttempt`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionAttempt", + "columnName": "submissionAttempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latePolicyStatus", + "columnName": "latePolicyStatus", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subAssignmentTag", + "columnName": "subAssignmentTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "submissionAttempt" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '371b12b510101fec6ff4b7155ac8b092')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt index c5518f2d68..abd9fe78a9 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt @@ -215,6 +215,209 @@ class CheckpointDaoTest { assertEquals("2025-10-10T00:00:00Z", entity.unlockAt) } + @Test + fun testFindByCourseIdWithModuleItem() = runTest { + setupCourseAndModule(1L, 100L) + + val checkpoint1 = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + val checkpoint2 = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_entry", + pointsPossible = 5.0, + dueAt = "2025-10-20T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + checkpointDao.insertAll(listOf(checkpoint1, checkpoint2)) + + val result = checkpointDao.findByCourseIdWithModuleItem(1L) + + assertEquals(2, result.size) + assertTrue(result.all { it.courseId == 1L }) + assertTrue(result.all { it.moduleItemId == 100L }) + } + + @Test + fun testFindByCourseIdWithModuleItemFiltersNullModuleItemIds() = runTest { + setupAssignment(1L, 1L) + + val checkpointWithAssignment = CheckpointEntity( + assignmentId = 1L, + name = null, + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = null, + courseId = 1L + ) + + checkpointDao.insert(checkpointWithAssignment) + + val result = checkpointDao.findByCourseIdWithModuleItem(1L) + + assertTrue(result.isEmpty()) + } + + @Test + fun testFindByCourseIdWithModuleItemFiltersOtherCourses() = runTest { + setupCourseAndModule(1L, 100L) + setupCourseAndModule(2L, 200L) + + val checkpoint1 = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + val checkpoint2 = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 200L, + courseId = 2L + ) + + checkpointDao.insertAll(listOf(checkpoint1, checkpoint2)) + + val result = checkpointDao.findByCourseIdWithModuleItem(1L) + + assertEquals(1, result.size) + assertEquals(1L, result[0].courseId) + assertEquals(100L, result[0].moduleItemId) + } + + @Test + fun testToModuleItemCheckpoint() { + val checkpointEntity = CheckpointEntity( + id = 1, + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + val moduleItemCheckpoint = checkpointEntity.toModuleItemCheckpoint() + + assertEquals("reply_to_topic", moduleItemCheckpoint.tag) + assertEquals(10.0, moduleItemCheckpoint.pointsPossible, 0.01) + assertTrue(moduleItemCheckpoint.dueAt != null) + } + + @Test + fun testToModuleItemCheckpointWithNullDueAt() { + val checkpointEntity = CheckpointEntity( + id = 1, + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = null, + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + val moduleItemCheckpoint = checkpointEntity.toModuleItemCheckpoint() + + assertEquals("reply_to_topic", moduleItemCheckpoint.tag) + assertEquals(10.0, moduleItemCheckpoint.pointsPossible, 0.01) + assertEquals(null, moduleItemCheckpoint.dueAt) + } + + @Test + fun testInsertCheckpointWithModuleItemId() = runTest { + setupCourseAndModule(1L, 100L) + + val checkpoint = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + checkpointDao.insert(checkpoint) + + val result = checkpointDao.findByCourseIdWithModuleItem(1L) + + assertEquals(1, result.size) + assertEquals(100L, result[0].moduleItemId) + assertEquals(null, result[0].assignmentId) + } + + @Test + fun testCascadeDeleteWithModuleItem() = runTest { + setupCourseAndModule(1L, 100L) + + val checkpoint = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + checkpointDao.insert(checkpoint) + + val moduleItemDao = db.moduleItemDao() + val moduleItem = moduleItemDao.findById(100L)!! + moduleItemDao.delete(moduleItem) + + val result = checkpointDao.findByCourseIdWithModuleItem(1L) + + assertTrue(result.isEmpty()) + } + private suspend fun setupAssignment(assignmentId: Long, courseId: Long) { val courseEntity = CourseEntity(Course(id = courseId)) courseDao.insert(courseEntity) @@ -228,4 +431,23 @@ class CheckpointDaoTest { ) assignmentDao.insert(assignmentEntity) } + + private suspend fun setupCourseAndModule(courseId: Long, moduleItemId: Long) { + val courseEntity = CourseEntity(Course(id = courseId)) + courseDao.insert(courseEntity) + + val moduleObjectDao = db.moduleObjectDao() + val moduleObjectEntity = com.instructure.pandautils.room.offline.entities.ModuleObjectEntity( + com.instructure.canvasapi2.models.ModuleObject(id = courseId, name = "Test Module"), + courseId + ) + moduleObjectDao.insert(moduleObjectEntity) + + val moduleItemDao = db.moduleItemDao() + val moduleItemEntity = com.instructure.pandautils.room.offline.entities.ModuleItemEntity( + com.instructure.canvasapi2.models.ModuleItem(id = moduleItemId), + moduleId = courseId + ) + moduleItemDao.insert(moduleItemEntity) + } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt index a0ee5c09d4..6b23c3c76e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt @@ -38,6 +38,7 @@ import com.instructure.canvasapi2.apis.QuizAPI import com.instructure.canvasapi2.apis.StudioApi import com.instructure.canvasapi2.apis.UserAPI import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.ModuleManager import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.offline.offlinecontent.CourseFileSharedRepository import com.instructure.pandautils.features.offline.sync.AggregateProgressObserver @@ -45,6 +46,7 @@ import com.instructure.pandautils.features.offline.sync.CourseSync import com.instructure.pandautils.features.offline.sync.FileSync import com.instructure.pandautils.features.offline.sync.HtmlParser import com.instructure.pandautils.features.offline.sync.StudioSync +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.CourseFeaturesDao import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao @@ -152,7 +154,9 @@ class OfflineSyncModule { customGradeStatusDao: CustomGradeStatusDao, customGradeStatusesManager: CustomGradeStatusesManager, plannerApi: PlannerAPI.PlannerInterface, - plannerItemDao: PlannerItemDao + plannerItemDao: PlannerItemDao, + checkpointDao: CheckpointDao, + moduleManager: ModuleManager ): CourseSync { return CourseSync( courseApi, @@ -192,7 +196,9 @@ class OfflineSyncModule { fileSync, customGradeStatusDao, customGradeStatusesManager, - plannerItemDao + plannerItemDao, + checkpointDao, + moduleManager ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt index 1a5b9b51dc..e9913db3d7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt @@ -36,6 +36,7 @@ import com.instructure.canvasapi2.apis.QuizAPI import com.instructure.canvasapi2.apis.UserAPI import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.ModuleManager import com.instructure.canvasapi2.models.AssignmentGroup import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Conference @@ -50,7 +51,9 @@ import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.depaginate +import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.features.offline.offlinecontent.CourseFileSharedRepository +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.CourseFeaturesDao import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao @@ -59,6 +62,7 @@ import com.instructure.pandautils.room.offline.daos.FileFolderDao import com.instructure.pandautils.room.offline.daos.PageDao import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.entities.CheckpointEntity import com.instructure.pandautils.room.offline.entities.CourseFeaturesEntity import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity @@ -119,7 +123,9 @@ class CourseSync( private val fileSync: FileSync, private val customGradeStatusDao: CustomGradeStatusDao, private val customGradeStatusesManager: CustomGradeStatusesManager, - private val plannerItemDao: PlannerItemDao + private val plannerItemDao: PlannerItemDao, + private val checkpointDao: CheckpointDao, + private val moduleManager: ModuleManager ) { private val additionalFileIdsToSync = mutableMapOf>() @@ -567,6 +573,33 @@ class CourseSync( ModuleItem.Type.Quiz.name -> fetchQuizModuleItem(courseId, it, params) } } + + fetchModuleItemCheckpoints(courseId) + } + } + + private suspend fun fetchModuleItemCheckpoints(courseId: Long) { + try { + val checkpointsWithModuleItems = moduleManager.getModuleItemCheckpoints(courseId.toString(), true) + val checkpointEntities = checkpointsWithModuleItems.flatMap { moduleItemWithCheckpoints -> + moduleItemWithCheckpoints.checkpoints.map { checkpoint -> + CheckpointEntity( + assignmentId = null, + name = null, + tag = checkpoint.tag, + pointsPossible = checkpoint.pointsPossible, + dueAt = checkpoint.dueAt?.toApiString(), + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = moduleItemWithCheckpoints.moduleItemId.toLongOrNull(), + courseId = courseId + ) + } + } + checkpointDao.insertAll(checkpointEntities) + } catch (e: Exception) { + firebaseCrashlytics.recordException(e) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt index b500e3c633..4cee8ae02e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt @@ -236,7 +236,7 @@ import com.instructure.pandautils.room.offline.entities.UserEntity CustomGradeStatusEntity::class, CheckpointEntity::class, SubAssignmentSubmissionEntity::class - ], version = 7 + ], version = 8 ) @TypeConverters(value = [Converters::class, OfflineConverters::class]) abstract class OfflineDatabase : RoomDatabase() { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt index 51ea4c1135..9d56c114f4 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt @@ -185,5 +185,29 @@ val offlineDatabaseMigrations = arrayOf( "`plannerOverrideMarkedComplete` INTEGER," + "FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON DELETE CASCADE)" ) + }, + createMigration(7, 8) { database -> + database.execSQL( + "CREATE TABLE IF NOT EXISTS `CheckpointEntity_temp` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + + "`assignmentId` INTEGER," + + "`name` TEXT," + + "`tag` TEXT," + + "`pointsPossible` REAL," + + "`dueAt` TEXT," + + "`onlyVisibleToOverrides` INTEGER NOT NULL," + + "`lockAt` TEXT," + + "`unlockAt` TEXT," + + "`moduleItemId` INTEGER," + + "`courseId` INTEGER," + + "FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE," + + "FOREIGN KEY(`moduleItemId`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)" + ) + database.execSQL( + "INSERT INTO CheckpointEntity_temp (id, assignmentId, name, tag, pointsPossible, dueAt, onlyVisibleToOverrides, lockAt, unlockAt) " + + "SELECT id, assignmentId, name, tag, pointsPossible, dueAt, onlyVisibleToOverrides, lockAt, unlockAt FROM CheckpointEntity" + ) + database.execSQL("DROP TABLE CheckpointEntity") + database.execSQL("ALTER TABLE CheckpointEntity_temp RENAME TO CheckpointEntity") } ) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt index 5ed1e092de..23d56df910 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CheckpointDao.kt @@ -37,4 +37,7 @@ interface CheckpointDao { @Query("DELETE FROM CheckpointEntity WHERE assignmentId = :assignmentId") suspend fun deleteByAssignmentId(assignmentId: Long) + + @Query("SELECT * FROM CheckpointEntity WHERE courseId = :courseId AND moduleItemId IS NOT NULL") + suspend fun findByCourseIdWithModuleItem(courseId: Long): List } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt index 52ea48ba73..028850e2cd 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CheckpointEntity.kt @@ -20,7 +20,9 @@ package com.instructure.pandautils.room.offline.entities import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint import com.instructure.canvasapi2.models.Checkpoint +import com.instructure.canvasapi2.utils.toDate @Entity( foreignKeys = [ @@ -28,21 +30,29 @@ import com.instructure.canvasapi2.models.Checkpoint entity = AssignmentEntity::class, parentColumns = ["id"], childColumns = ["assignmentId"], - onDelete = ForeignKey.CASCADE + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = ModuleItemEntity::class, + parentColumns = ["id"], + childColumns = ["moduleItemId"], + onDelete = ForeignKey.CASCADE, ) ] ) data class CheckpointEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, - val assignmentId: Long, + val assignmentId: Long? = null, val name: String?, val tag: String?, val pointsPossible: Double?, val dueAt: String?, val onlyVisibleToOverrides: Boolean, val lockAt: String?, - val unlockAt: String? + val unlockAt: String?, + val moduleItemId: Long? = null, + val courseId: Long? = null ) { constructor(checkpoint: Checkpoint, assignmentId: Long) : this( assignmentId = assignmentId, @@ -65,4 +75,12 @@ data class CheckpointEntity( lockAt = lockAt, unlockAt = unlockAt ) + + fun toModuleItemCheckpoint(): ModuleItemCheckpoint { + return ModuleItemCheckpoint( + dueAt = dueAt?.toDate(), + tag = tag.orEmpty(), + pointsPossible = pointsPossible ?: 0.0 + ) + } } From 97939f69e2833cc0ef51d64035455dcf9ad02d23 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:16:39 +0200 Subject: [PATCH 52/94] [MBL-19388][All] Remove app theme selector dialog (#3322) refs: MBL-19388 affects: Student, Teacher, Parent release note: App theme now defaults to system theme on first launch, eliminating the theme selection dialog. * Removed app theme dialog. * Reverted test rule --- .../parentapp/utils/ParentActivityTestRule.kt | 4 - .../parentapp/di/ApplicationModule.kt | 3 - .../parentapp/util/FlutterAppMigration.kt | 13 -- .../parentapp/util/FlutterAppMigrationTest.kt | 13 -- .../ui/utils/StudentActivityTestRule.kt | 8 +- .../student/activity/NavigationActivity.kt | 7 - .../ui/utils/TeacherActivityTestRule.kt | 4 - .../teacher/activities/InitActivity.kt | 7 - .../themeselector/ThemeSelectorBottomSheet.kt | 79 ---------- .../pandautils/utils/ThemePrefs.kt | 6 +- .../layout/bottom_sheet_theme_selector.xml | 146 ------------------ 11 files changed, 4 insertions(+), 286 deletions(-) delete mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/themeselector/ThemeSelectorBottomSheet.kt delete mode 100644 libs/pandautils/src/main/res/layout/bottom_sheet_theme_selector.xml diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentActivityTestRule.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentActivityTestRule.kt index 39aa54cdd1..89045f015e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentActivityTestRule.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentActivityTestRule.kt @@ -23,7 +23,6 @@ import com.instructure.espresso.InstructureActivityTestRule import com.instructure.loginapi.login.util.LoginPrefs import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.utils.PandaAppResetter -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.parentapp.util.ParentPrefs @@ -34,8 +33,5 @@ class ParentActivityTestRule(activityClass: Class) : Instructur ParentPrefs.clearPrefs() PreviousUsersUtils.clear(context) LoginPrefs.clearPrefs() - - // We need to set this true so the theme selector won't stop our tests. - ThemePrefs.themeSelectionShown = true } } 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 529133a410..50d7b1d9cb 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 @@ -26,7 +26,6 @@ import com.instructure.pandautils.dialogs.RatingDialog import com.instructure.pandautils.features.reminder.ReminderRepository import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao import com.instructure.pandautils.utils.LogoutHelper -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.parentapp.util.FlutterAppMigration import com.instructure.parentapp.util.ParentLogoutHelper import com.instructure.parentapp.util.ParentPrefs @@ -73,7 +72,6 @@ class ApplicationModule { @Provides fun provideFlutterAppMigration( @ApplicationContext context: Context, - themePrefs: ThemePrefs, parentPrefs: ParentPrefs, loginPrefs: LoginPrefs, previousUsersUtils: PreviousUsersUtils, @@ -85,7 +83,6 @@ class ApplicationModule { ): FlutterAppMigration { return FlutterAppMigration( context, - themePrefs, parentPrefs, loginPrefs, previousUsersUtils, diff --git a/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt b/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt index adee092bea..2762e3b6ec 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt @@ -37,7 +37,6 @@ import com.instructure.pandautils.dialogs.RatingDialog import com.instructure.pandautils.features.reminder.ReminderRepository import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao import com.instructure.pandautils.room.calendar.entities.CalendarFilterEntity -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.fromJson import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.qualifiers.ApplicationContext @@ -95,7 +94,6 @@ data class FlutterSignedInUser( class FlutterAppMigration( @ApplicationContext private val context: Context, - private val themePrefs: ThemePrefs, private val parentPrefs: ParentPrefs, private val loginPrefs: LoginPrefs, private val previousUsersUtils: PreviousUsersUtils, @@ -108,22 +106,11 @@ class FlutterAppMigration( fun migrateIfNecessary() { if (!parentPrefs.hasMigrated) { parentPrefs.hasMigrated = true - migratePrefs() migrateEncryptedSharedPrefs() migrateDatabase() } } - private fun migratePrefs() = try { - val prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) - - val isDarkMode = prefs.getBoolean(KEY_DARK_MODE, false) - - themePrefs.appTheme = if (isDarkMode) 1 else 0 - } catch (e: Exception) { - e.printStackTrace() - } - private fun migrateEncryptedSharedPrefs() = try { val masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) diff --git a/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt index fb7ae99a86..1d1c4aad39 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt @@ -35,7 +35,6 @@ import com.instructure.pandautils.dialogs.RatingDialog import com.instructure.pandautils.features.reminder.ReminderRepository import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao import com.instructure.pandautils.room.calendar.entities.CalendarFilterEntity -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.parentapp.R import io.mockk.coVerify import io.mockk.every @@ -53,7 +52,6 @@ import java.time.ZoneId class FlutterAppMigrationTest { private val context: Context = mockk(relaxed = true) - private val themePrefs: ThemePrefs = mockk(relaxed = true) private val parentPrefs: ParentPrefs = mockk(relaxed = true) private val loginPrefs: LoginPrefs = mockk(relaxed = true) private val previousUsersUtils: PreviousUsersUtils = mockk(relaxed = true) @@ -69,7 +67,6 @@ class FlutterAppMigrationTest { private val flutterAppMigration = FlutterAppMigration( context, - themePrefs, parentPrefs, loginPrefs, previousUsersUtils, @@ -134,16 +131,6 @@ class FlutterAppMigrationTest { coVerify(exactly = 0) { parentPrefs.hasMigrated = true } } - @Test - fun `Migrate dark mode setting`() { - every { mockSharedPreferences.getBoolean("flutter.dark_mode", false) } returns true - every { context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) } returns mockSharedPreferences - - flutterAppMigration.migrateIfNecessary() - - coVerify(exactly = 1) { themePrefs.appTheme = 1 } - } - @Test fun `Migrate last user`() { every { mockSharedPreferences.all } returns mapOf( diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt index f14c13ddba..7106bad06b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt @@ -18,13 +18,12 @@ package com.instructure.student.ui.utils import android.app.Activity import android.content.Context -import com.instructure.student.util.CacheControlFlags -import com.instructure.student.util.StudentPrefs import com.instructure.espresso.InstructureActivityTestRule import com.instructure.loginapi.login.util.LoginPrefs import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.utils.PandaAppResetter -import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.student.util.CacheControlFlags +import com.instructure.student.util.StudentPrefs class StudentActivityTestRule(activityClass: Class) : InstructureActivityTestRule(activityClass) { @@ -34,9 +33,6 @@ class StudentActivityTestRule(activityClass: Class) : Instructu CacheControlFlags.clearPrefs() PreviousUsersUtils.clear(context) LoginPrefs.clearPrefs() - - // We need to set this true so the theme selector won't stop our tests. - ThemePrefs.themeSelectionShown = true } } diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt index 2f9e4a8010..94d384b33d 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt @@ -89,7 +89,6 @@ import com.instructure.pandautils.features.notification.preferences.PushNotifica import com.instructure.pandautils.features.offline.sync.OfflineSyncHelper import com.instructure.pandautils.features.reminder.AlarmScheduler import com.instructure.pandautils.features.settings.SettingsFragment -import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomSheet import com.instructure.pandautils.interfaces.NavigationCallbacks import com.instructure.pandautils.models.PushNotification import com.instructure.pandautils.receivers.PushExternalReceiver @@ -385,12 +384,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. val savedBottomScreens = savedInstanceState?.getStringArrayList(BOTTOM_SCREENS_BUNDLE_KEY) restoreBottomNavState(savedBottomScreens) - if (!ThemePrefs.themeSelectionShown) { - val themeSelector = ThemeSelectorBottomSheet() - themeSelector.show(supportFragmentManager, ThemeSelectorBottomSheet::javaClass.name) - ThemePrefs.themeSelectionShown = true - } - requestNotificationsPermission() networkStateProvider.isOnlineLiveData.observe(this) { isOnline -> diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt index aa47deb4fc..9a5d28b3c1 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt @@ -22,7 +22,6 @@ import com.instructure.espresso.InstructureActivityTestRule import com.instructure.loginapi.login.util.LoginPrefs import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.utils.PandaAppResetter -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.teacher.utils.TeacherPrefs class TeacherActivityTestRule(activityClass: Class) : InstructureActivityTestRule(activityClass) { @@ -32,9 +31,6 @@ class TeacherActivityTestRule(activityClass: Class) : Instructur TeacherPrefs.safeClearPrefs() PreviousUsersUtils.clear(context) LoginPrefs.clearPrefs() - - // We need to set this true so the theme selector won't stop our tests. - ThemePrefs.themeSelectionShown = true } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt index 631c9a6181..84ca934b28 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt @@ -77,7 +77,6 @@ import com.instructure.pandautils.features.inbox.list.OnUnreadCountInvalidated import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.features.reminder.AlarmScheduler import com.instructure.pandautils.features.settings.SettingsFragment -import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomSheet import com.instructure.pandautils.interfaces.NavigationCallbacks import com.instructure.pandautils.models.PushNotification import com.instructure.pandautils.receivers.PushExternalReceiver @@ -246,12 +245,6 @@ class InitActivity : BasePresenterActivity. - * - */ -package com.instructure.pandautils.features.themeselector - -import android.content.res.Configuration -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.widget.CompoundButtonCompat -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.instructure.pandautils.R -import com.instructure.pandautils.base.BaseCanvasBottomSheetDialogFragment -import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.databinding.BottomSheetThemeSelectorBinding -import com.instructure.pandautils.utils.AppTheme -import com.instructure.pandautils.utils.ColorKeeper -import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.onClick - -class ThemeSelectorBottomSheet : BaseCanvasBottomSheetDialogFragment() { - - private val binding by viewBinding(BottomSheetThemeSelectorBinding::bind) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? = inflater.inflate(R.layout.bottom_sheet_theme_selector, container, false) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) { - super.onViewCreated(view, savedInstanceState) - - val radioButtonColor = ViewStyler.makeColorStateListForRadioGroup(requireContext().getColor(R.color.textDarkest), requireContext().getColor(R.color.textInfo)) - CompoundButtonCompat.setButtonTintList(buttonLightTheme, radioButtonColor) - CompoundButtonCompat.setButtonTintList(buttonDarkTheme, radioButtonColor) - CompoundButtonCompat.setButtonTintList(buttonDeviceTheme, radioButtonColor) - - saveButton.onClick { - val appTheme = when { - buttonLightTheme.isChecked -> AppTheme.LIGHT - buttonDarkTheme.isChecked -> AppTheme.DARK - else -> AppTheme.SYSTEM - } - setAppTheme(appTheme) - } - - (dialog as? BottomSheetDialog)?.behavior?.state = BottomSheetBehavior.STATE_EXPANDED - } - - private fun setAppTheme(appTheme: AppTheme) { - AppCompatDelegate.setDefaultNightMode(appTheme.nightModeType) - ThemePrefs.appTheme = appTheme.ordinal - - val nightModeFlags: Int = requireContext().resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - ColorKeeper.darkTheme = nightModeFlags == Configuration.UI_MODE_NIGHT_YES - ThemePrefs.isThemeApplied = false - - dismiss() - } - -} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ThemePrefs.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ThemePrefs.kt index c20d87bb29..bb811de176 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ThemePrefs.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ThemePrefs.kt @@ -63,13 +63,11 @@ object ThemePrefs : PrefManager("CanvasTheme") { var isThemeApplied by BooleanPref() - var appTheme by IntPref(defaultValue = 0) - - var themeSelectionShown by BooleanPref() + var appTheme by IntPref(defaultValue = 2) // Default to system private var canvasTheme: CanvasTheme? = null - override fun keepBaseProps() = listOf(::appTheme, ::themeSelectionShown) + override fun keepBaseProps() = listOf(::appTheme) override fun onClearPrefs() { } diff --git a/libs/pandautils/src/main/res/layout/bottom_sheet_theme_selector.xml b/libs/pandautils/src/main/res/layout/bottom_sheet_theme_selector.xml deleted file mode 100644 index e37f2861b3..0000000000 --- a/libs/pandautils/src/main/res/layout/bottom_sheet_theme_selector.xml +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - -